跳转至

2 Class

文本统计:约 6683 个字 • 516 行代码

2.1 Basic Concepts

C++中的对象=属性+操作 objects=attributes+operations,对于面向对象的编程而言,任何东西都是一个对象,任何对象都有对应的类型,程序就是一系列对象互相之间传递信息来完成功能

  • C++的类包含成员变量和成员函数
  • C++中class的定义,具体实现和调用可以分成三个文件,头文件header是具体实现和调用之间的接口,每个类的定义需要用1个头文件

Compilation unit: A .cpp file is a compile unit, The compiler sees only one .cpp file and generates a corresponding .obj file. 每个cpp文件是一个编译单元

The linker links all .obj files into one executable file.

To provide information across .cpp files, use .h file.由于每个.cpp文件是独立编译的,它们之间无法直接访问对方的定义和声明。因此,需要通过.h(头)文件来共享这些信息。

Structure of C++ program:

Header = interface

The header is a contract between the author and the user of the code.

  • declaration of functions, classes, variables, etc.

  • all definitions go to .cpp files.

The compiler enforces the contract.

#include

#include is to insert the content of header file into the .cpp file, right at where the #include statement is.

  • #include "xx.h" : usually search in the current directory, implementation defined
  • #include <xx.h> : search in the specified directories, depending on your development environment.

:: resolver

::操作符可以用来访问类中的内容

  • ::

  • ::

void S::f() {
 ::f(); // 调用全局函数 f(),防止递归调用类成员函数 f()
 ::a++; // Select the global 'a'
 a--; // The 'a' at class scope
}

Standard header file structure

#ifndef HEADER_FLAG
#define HEADER_FLAG
// all kinds of declarations here...
#endif // HEADER_FLAG

宏保护的作用

宏保护,通常指的是在头文件(.h)中使用预处理指令来防止重复包含(也称为“多重包含”)。这种技术可以避免因多次包含同一个头文件而导致的编译错误。具体来说,宏保护通过定义一个唯一的宏标识符来检查该头文件是否已经被包含过。如果已经被包含,则跳过该头文件的内容;如果没有被包含,则继续处理并定义该宏标识符以供后续检查。

2.2 Ctor and Dtor

Constructor 构造函数

构造函数的函数名和类的名字相同,可以传入一些参数用来初始化一个对象,在类的对象被定义的时候会自动调用构造函数

default constructor 默认构造函数,不需要参数也可以使用的构造函数,或者说没有写相应的构造函数,编译器会帮我们自动生成一份默认构造函数

初始化列表:在函数签名后面,大括号之前直接对类中定义的变量进行赋值 - const类型的成员变量初始化只能用初始化列表 - 构造函数的执行分为两个阶段:初始化阶段函数执行阶段,会先执行初始化列表里的赋值,在进入函数主体进行对应的操作 - 初始化顺序取决于成员变量的声明顺序:无论你在初始化列表中如何排列成员变量的初始化顺序,实际的初始化顺序总是按照成员变量在类中声明的顺序来进行。 - 析构顺序是初始化顺序的逆序:当对象被销毁时,成员变量的析构顺序与它们的初始化顺序相反。也就是说,如果x先于y被初始化,那么在对象销毁时,y会先于x被析构。

class Point {
private:
 const float x, y;
public:
 Point(float xa, float ya)
 : y(ya), x(xa) {}
};

Destructor 析构函数

析构函数的函数名是类名前面加一个~,析构函数不需要参数,在类的生命周期结束的时候会被编译器自动调用

2.3 static 类型

之前已经讲到了static类型的全局变量只在当前文件有效,不能通过extern跨文件调用,而函数中的static类型的变量在第一次调用的时候会被初始化,之后再调用该函数这个static类型的变量保持上一次函数调用结束时的值

#include<iostream>
using namespace std;

class A{
    public:
        A() {cout<<"A::A()"<<endl;}
        ~A(){cout<<"A::~A()"<<endl;}
}

void f(int n)
{
    if(x>10)
        static A a;
    cout<<"f()"<<endl;
}
int main()
{
    cout<<"start"<<endl;
    f(1);
    f(11);
    return 0;
}
//此时只有在第二次调用时a才会被构造
//无论什么情况,A的构造和析构函数只会被执行一次

类中的成员(变量和函数)分为两种

  • 静态成员:在类内所有对象之间共享

  • 实例成员:只能在某个具体的对象中调用

  • 静态函数不能访问实例成员

类中定义的static类型的变量是静态成员变量,其值会在这个类的所有成员之间共享

non-const类型的静态成员变量需要在类的外面进行定义,不定义的话会出现错误,比如:

#include <iostream>
using namespace std;

class A {
public:
    static int count;
    A() {
        A::count++;
    }
};

int A::count = 0; // 在类的外部赋值的时候不需要说明static,但是需要注明A::,否则就是一个新的变量

int main()
{
    A* array = new A[100];
    cout<<A::count<<endl;
}

const类型的静态成员变量作为类内共享的一个常量,也需要在类的外部进行定义【对于整型(包括int, char, bool等)和枚举类型的const static成员变量,可以在类内部直接进行初始化(自C++11起)。】,此时要写出关键字const,并且这个静态成员变量是不能被改变的。虽然const static成员变量是常量,但它们仍然可以通过类名或对象来访问。

静态成员函数:

  • 静态成员函数只能访问静态成员变量和静态成员函数,不能直接访问类的非静态成员(包括成员变量和成员函数)。

  • 需要在函数定义之前加static关键字

  • 虽然静态成员函数可以继承,但它们不会像普通成员函数那样参与多态性。也就是说,子类不能重写基类的静态成员函数。当你试图通过基类的引用或指针调用一个静态成员函数时,它将总是调用该函数在基类中的版本。

class Base {
public:
    static void show() { cout << "Base class" << endl; }
};

class Derived : public Base {
public:
    static void show() { cout << "Derived class" << endl; } // 不会覆盖Base::show()
};

int main() {
    Base* b = new Derived();
    b->show(); // 输出:"Base class"
}

2.4 this 指针

定义:在非静态成员函数内部,this是一个隐式包含的指针,它指向调用该函数的对象。

类型this的类型是“指向类类型的常量指针”,即如果有一个类ClassA,那么this的类型就是ClassA* const

不可更改性:由于this是指向当前对象的常量指针,你不能让它指向其他对象(即不能修改this指针本身)。

使用场景有

(1)区分成员变量和参数,当成员函数的参数名与成员变量名相同时,可以使用this指针来区分两者

class Example {
private:
    int value;
public:
    void setValue(int value) {
        this->value = value; // 使用this指针区分成员变量和函数参数
    }
};

(2)返回当前对象:可以利用this指针返回当前对象实例,这在链式编程风格中特别有用

class ChainExample {
public:
    ChainExample& setX(int x) {
        // 设置一些属性...
        return *this; // 返回当前对象的引用
    }

    ChainExample& setY(int y) {
        // 设置一些属性...
        return *this;
    }
};

// 使用示例
ChainExample obj;
obj.setX(10).setY(20); // 链式调用

(3)显式传递当前对象:在某些情况下,可能需要显式地将当前对象作为参数传递给其他函数或方法

void someFunction(const Example* example);

class Example {
public:
    void callSomeFunction() {
        someFunction(this); // 将当前对象传递给someFunction
    }
};

注意:静态成员函数不包含this指针,因为它们属于类本身而不是某个具体的对象实例。

由于this是指向当前对象的常量指针,因此不能改变它的值。这意味着你不能让this指向另一个对象。

this: the hidden parameter

this is a hidden parameter for all member functions, with the type of the class.

  • void Point::print()➔ (can be regarded as) void Point::print(Point *this)

To call the function, you must specify a variable.

  • Point a; a.print();➔ (can be regarded as) Point::print(&a);

Inside member functions, you can use this as the pointer to the variable that calls the function.

2.5 Constant object

需要加const声明,在声明之后就不能改变这个对象内部变量的值,会有一些成员函数不能正常使用

在成员函数参数表后面加const可以成为const型成员函数,const类型的成员函数不能修改成员变量的值

const 声明写在函数的开头表示函数的返回值类型是const

const声明写在函数签名的末尾表示这个成员函数不能修改类中定义的成员变量,被称为常成员函数

  • 但是如果是成员变量中有指针,并不能保证指针指向的内容不被修改

  • const类型的函数和非const类型的函数也可以构成重载关系,比如:

class A {
public:
    void foo() {
        cout << "A::foo();" << endl;
    }
    void foo() const {
        cout << "A::foo() const;" << endl;
    }
};

int main()
{
    A a;
    a.foo(); //访问的是非const类型的foo
    const A aa;
    aa.foo(); //访问的是const类型的foo
    return 0;
}

const类型的成员函数的使用规则如下:

  • 只有const成员函数能够被常量对象调用。
  • const类型函数不会改变任何成员变量的值
  • 构成重载关系的时候,const类型的对象只能调用const类型的成员函数,不能调用non-const,而非const类型的对象优先调用non-const的成员函数,如果没有non-const再调用const类型的

对于 Constant members 来说,必须要在构造函数的初始化列表中被初始化

class HasArray {
 const int size;
 int array[size]; // ERROR!
 ...
};

这里错误的原因是对于 constant members 来说,初始化是在实例化的时候,所以这里数组的大小并不是确定的,我们可以选择使用 static const int size = 100; 这个就是针对于整个类来说的。

2.6 Function Overloading

Overhead for a function call

The extra processing time required:

Push parameters, Push return address, Prepare return values, Pop all pushed

函数名相同而参数的个数和类型不同的几个函数构成重载关系,一个类可以有多个不同的构造函数来解决不同情况下的构造

default value:缺省值,可以在函数参数表中直接声明一些参数的值,但是必须要从右往左,当传入的参数缺省时函数默认将已经声明的值作为参数的值

2.7 Inline Function

需要在函数名前面加关键字inline, 声明为内联函数

内联函数在编译期会被编译器在调用处直接扩展为一个完整的函数,因此可以减少运行时调用函数的cost,类似于宏,但是比宏更加安全,会检查类型。本质是空间换时间

内联函数的定义和函数主体部分都应该写在头文件中,由于inline函数的定义被包含在多个源文件中,这可能会导致看起来像是同一个函数有多个定义的情况。然而,C++标准规定,对于inline函数,即使在多个翻译单元(即编译后的源文件)中出现相同的定义,只要这些定义完全相同,就不会被视为重复定义错误。编译器和链接器会处理这种情况,确保最终生成的可执行文件中只有一个这样的函数实现。【比较一下普通函数和inline函数的区别】

class中的函数都是默认inline

2.8 Inheritance

本来这里前面还有一个 Composition 的内容,但是这个应该不难,只需要知道相应构造函数的写法即可。下面着重介绍 Inheritance

Inheritance:从基类中继承生成派生类

派生类继承了基类的所有变量和成员函数

派生类中不能直接访问基类的private的变量和成员函数,具体的作用域与访问权限如下表格(假设为 public 继承)注意这些都是针对于类的,而不是对象,对于private的部分,不同对象之间也可以相互访问

specifiers within same class in derived class outside the class
private Yes No No
protected Yes Yes No
public Yes Yes Yes

Friend

友元函数

  • 在类中声明一个全局函数或者其他类的成员函数为friend
  • 可以使这些函数拥有访问类内private和protected类型的变量和函数的权限
  • 友元函数也可以是一个类,这种情况下被称为是友元类,整个类和所有的成员都是友元
  • 友元函数本身不是那个类的成员函数,函数签名里不需要className::来表示是这个类的成员函数,直接作为普通函数即可
class A {
private:
    int val;
public:
    A(int value): val(value) {
        cout<<"A()"<<endl;
    }
    friend void showValue(A a);
};

void showValue(A a)
{
    cout<<a.val<<endl;
}

后面我们以 employee 和 manager 为例展开,先声明 employee 类的定义

class Employee {
public:
 Employee(const string& name, const string& ssn);
 const string& get_name() const;
 void print(ostream& out) const;
 void print(ostream& out, const string& msg) const;
protected:
 string m_name;
 string m_ssn;
};

继承类 Manager

class Manager : public Employee {
public:
 Manager(const string& name, 
 const string& ssn, 
 const string& title);
 const string title_name() const;
 const string& get_title() const;
 void print(ostream& out) const;
private:
 string m_title;
};

派生类的构造函数,可以在派生类的构造函数中调用基类的构造函数

Manager::Manager(const string& name,
 const string& ssn,
 const string& title = "")
 : Employee(name, ssn), m_title(title)
{
}

派生类在被构造的时候会先调用基类的构造函数,再调用派生类的构造函数,析构的时候先调用派生类的析构函数,再调用基类的析构函数

  • 如果派生类没有定义构造函数,则直接调用基类的构造函数
  • 如果派生类定义了构造函数,在执行之前会先调用基类的构造函数,如果派生类的构造函数中没有显式调用基类的构造函数,则会选择调用基类的默认无参构造函数

  • 当继承和组合两种情况同时出现时,先构造基类,再构造派生类中组合的其他类,再构造派生类,析构的时候类似

  • Base class is always constructed first

  • 就算组合的类在初始化列表或者构造函数中没有调用构造函数,C++编译器也会自动调用这个类默认的构造函数

  • 派生类中可以对基类函数进行重载,此时如果派生类对象调用对应的函数按照派生类中的同名函数执行

name hiding

If you redefine a member function in the derived class, all the other overloaded functions in the base class are inaccessible. 如果在继承类中重新定义了成员函数,那么基类中的所有同名函数都被隐藏了

int main () {
 Employee bob( "Bob Jones", "555-44-0000" );
 Manager bill( "Bill Smith", "666-55-1234", 
 "ImportantPerson" );
 // okay Manager inherits Employee
 string name = bill.get_name(); 
 // Error -- bob is an Employee!
 string title = bob.get_title(); 
 cout << bill.title_name() << '\n' << endl;
 bob.print(cout);
 bob.print(cout, "Employee:");
 bill.print(cout);
 bill.print(cout, "Employee:"); // Error -- hidden!
}

class和struct的区别:class中的变量和函数默认为private,struct中的函数默认为public

访问控制如下表格 (Suppose class B is derived from class A.)

inheritance type (B is) public members protected members private members
: private A private in B private in B not accessible
: protected A protected in B protected in B not accessible
: public A public in B protected in B not accessible

继承的种类:public,private,protected继承,继承之后的基类变量的访问控制取原本类型和继承类型中较严格的

Conversions:

  • If B is-a A, you can use a B anywhere an A can be used.
  • if B is-a A, then everything that is true for A is also true of B.
Manager pete("Pete", "444-55-6666", "Bakery");
Employee * ep = &pete; // Upcast
Employee & er = pete; // Upcast

调用 ep->print(cout); 时调用的就是基类的函数

2.9 Polymorphism

多态 Polymorphism

  • 同一段代码可以产生不同效果

  • 对于继承体系中的某一系列同名函数,不同的类型会调用不用的函数

  • 一般情况下,有继承关系的类之间有函数构成重载关系,依然会根据变量类型来调用对应的函数,比如:

#include <iostream>
using namespace std;

class A{
public:
    virtual void foo() {
        cout<<1<<endl;
    }
};

class B: public A{
public:
    virtual void foo() {
        cout<<2<<endl;
    }
};

int main()
{
    A a;
    B b;
    a.foo();
    b.foo();
    return 0;
}
  • 此时运行的结果是1和2,即A型的变量的foo函数是基类中的,B类型的变量的foo函数是派生类中的

Static Binding

函数的调用在程序开始运行之前就已经确定了

对于像下面这样的情况,基类的指针(引用)指向派生类,并且调用了基类中也存在的同名函数,最终调用的都是基类的同名函数

class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      int area()
      {
         cout << "Parent class area :" <<endl;
         return 0;
      }
};
class Rectangle: public Shape{
   public:
      Rectangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      { 
         cout << "Rectangle class area :" <<endl;
         return (width * height); 
      }
};
class Triangle: public Shape{
   public:
      Triangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      { 
         cout << "Triangle class area :" <<endl;
         return (width * height / 2); 
      }
};

int main( )
{
   Shape *shape;
   Rectangle rec(10,7);
   Triangle  tri(10,5);
   shape = &rec;
   shape->area();
   shape = &tri;
   shape->area();
   return 0;
}

Virtual Function

一种用于实现多态的机制,核心理念是通过基类访问派生类定义的函数,这种方式称为动态链接

用于区分派生类中和基类同名的方法函数,需要将基类的成员函数类型声明为virtual

  • 基类中的析构函数一定要为虚函数,否则会出现对象释放错误

纯虚函数virtual int func() = 0; 表明该函数没有主体,基类中没有给出有意义的实现方式,需要在派生类中进行扩展

override语法:派生类中可以用override关键字来声明,表示对基类虚函数的重载【未重载会报错】

虚函数需要借助指针引用达到多态的效果

  • 如果基类指针/引用指向基类,那就正常调用基类的相关成员函数
  • 如果基类指针指向派生类,则调用的时候会调用派生类的成员函数
  • 派生类指针不能指向基类
class A{
public:
    virtual void foo(){
        cout<<"A"<<endl;
    }
};

class B{
public:
    virtual void foo(){
        cout<<"B"<<endl;
    }
}

int main()
{
  A *a=new B();
  a->foo(); //结果为B
  return 0;
}

虚函数的实现方式:虚函数表 virtual table

每一个有虚函数的类都会有一个虚函数表,该类的任何对象中都存放着虚函数表的指针,虚函数表中列出了该类的虚函数地址

  • 虚函数表是一个指针数组,里面存放了一系列虚函数的指针
  • 虚函数的调用需要经过虚函数表的查询,非虚函数的调用不需要经过虚函数表
  • 虚函数表在代码的编译阶段就完成了构造
  • 一个类只有一张虚函数表,每一个对象都有指向虚函数表的一个指针vptr

多态的函数调用语句被编译成一系列根据基类指针所指向的对象存放的虚函数表的地址,在从虚函数表中查找地址调用对应的虚函数

Conversions

Ellipse elly(20f, 40f);
Circle circ(60f);
elly = circ;

假设 Circle 继承于 Ellipse ,将 Circle 对象赋值给 Ellipse 对象的时候会发生 Object Slicing

  • circ 的部分数据被复制到 elly 中,但只限于 Ellipse 类型的公共部分。

  • elly 的 vptr 依然指向 Ellipse 的 vtable,保持了其静态和动态类型的一致性。

但是对于指针来说,情况略有不同

Ellipse *elly = new Ellipse(20f, 40f);
Circle *circ = new Circle(60f);
elly = circ;

这样 elly 原先指向的 Ellipse 对象就丢失了,ellycirc 都指向了同一个 Circle 对象,调用 render() 调用的也是 Circle 的 render()

elly->render(); // Circle::render()

Return type relaxation

Suppose D is publicly derived from B

D::f() can return a subclass of the return type defined in B::f(),Applies to pointer and reference types

class Expr {
public:
 virtual Expr* newExpr();
 virtual Expr& clone();
 virtual Expr self();
};
class BinaryExpr : public Expr {
public:
 virtual BinaryExpr* newExpr(); // ok
 virtual BinaryExpr& clone(); // ok
 virtual BinaryExpr self(); // Error!
};

因为 BinaryExpr*BinaryExpr& 都可以隐式转化为 Expr*Expr&BinaryExpr 转化为 Expr 会发生 Object Slicing 的问题

Overloading and virtual

class Base {
public:
 virtual void func();
 virtual void func(int);
};

对于虚函数的重载问题,在派生类中重写了其中一个函数,就一定要重写其他函数,否则会出现名覆盖问题

Abstract classes

描述了一个类应该有的功能和行为,但是不用在这个类中实现,而是在派生类中实现

可以使用纯虚函数来实现抽象类的定义,比如:

class Shape {
public:
    virtual double getArea() = 0;
    Shape(int a, int b): length(a), width(b) {}
protected:
    int length;
    int width;
};

class Rectangle: public Shape {
public:
    double getArea() {
        return length * width;
    }
};

class Triangle: public Shape {
public:
    double getArea() {
        return length * width / 2;
    }
};

Summary

情况1:基类和派生类都不是virtual

  • 此时对于基类的对象和基类的指针,执行的就是基类的f,对于派生类的执行的就是派生类的f
class A {
public:
    void f() {
        cout << "af" << endl;
    }
};

class B: public A {
public:
    void f() {
        cout << "bf" <<endl;
    }
};

int main()
{
    A a;
    B b;
    a.f();
    b.f();
    A* pb = &b;
    pb->f();
}

情况2:派生类中的同名函数是虚函数:无影响,和1一模一样

情况3:基类中的是虚函数,派生类中不注明是虚函数:此时派生类的对象和指向派生类的指针执行的都是派生类的函数f,基类的对象和指向基类对象的指针执行的都是基类的函数f

总结:

(1)virtual的虚函数关键字是向下负责的,派生类声明virtual对基类无任何影响

(2)对于指针和引用而言

  • 不是虚函数的时候,调用的函数取决于指针和引用的变量类型(基类指针调用基类,派生类指针调用派生类)
  • 是虚函数的时候,调用函数取决于指针和引用指向的变量类型(指向基类调用基类,指向派生类调用派生类)
  • 当然如果派生类里没有新的同名函数,那么执行的都是基类里的
  • 要注意派生类指针不能直接指向基类的对象
  • 如果虚函数里还需要调用其他函数,调用的规则也和上面的一样,比如下面有个历年卷上面的神题:

历年卷上的一个题目:写出程序的输出

class B {
public:
    void f() {
        cout << "bf" << endl;
    }
    virtual void vf() {
        cout << "bvf" << endl;
    }
    void ff() {
        vf();
        f();
    }
    virtual void vff() {
        vf();
        f();
    }
};

class D: public B {
public:
    void f() {
        cout << "df" << endl;
    }
    void ff() {
        f();
        vf();
    }
    void vf() {
        cout << "dvf" <<endl;
    }
};

int main()
{
    D d;
    B* pb = &d;
    pb->f();
    pb->ff();
    pb->vf();
    pb->vff();
}

这道题的分析过程如下:

  • 首先调用f,而f不是虚函数,所以根据指针类型调用了B中的f,输出bf
  • 再调用ff,因为ff也不是虚函数,所以调用B中的ff,B中的ff调用了vf和f,而vf是虚函数,B类型指针指向的是D,所以调用D中的vf,输出dvf,调用f则和上面一样输出bf
  • 再调用vf,由于vf是虚函数,所以要调用D中的vf,输出dvf
  • 再调用vff,虽然是虚函数但是D中没有定义同名函数,所以调用B中的vff,vff中调用vf和f,同2一样输出的是dvf和bf
  • 所以最终的输出是
bf
dvf
bf
dvf
dvf
bf

这道题涵盖了单继承虚函数的所有情况

(3)基类虚函数的优先级高于派生类中的需要强制类型转换的同名函数

  • 比如下面一段代码中
class A {
public:
    virtual void f(int i) {
        cout << 1 <<endl;
    }
};

class B: public A {
public:
    virtual void f(double i) {
        cout << 2 << endl;
    }
};

int main()
{
    A* pa = new B;
    pa->f(1);
    return 0;
}
  • 这里输出的结果是1,事实上两个f并不构成虚函数的关系,因为f(1)中1是int类型,所以优先调用了对int匹配度高的
  • 事实上如果是f(1.1)输出的结果仍然是1,并且CLion会提示参数需要类型转换
  • 事实上两个f不构成虚函数的多态关系,所以调用哪个并不看指针指向的对象,而是看指针本身的类型!

2.10 Copy constructor

拷贝构造函数是利用一个已经有的对象拷贝构造出一个新的对象,并完成初始化的函数,函数签名像这样 T::T(const T&);

对于拷贝构造函数,如果成员变量中都是类定义的对象或者原生数据类型,默认的拷贝构造函数可以很好地完成。但是如果是指针的话,数据就会被共享,出现浅拷贝的问题

C++中的拷贝:浅拷贝深拷贝

(1)浅拷贝:在原来已有的内存中增加一个新的指针指向这一段内存

(2)深拷贝:分配一块新的内存,复制对应的值,并定义一个新的指针指向这一块内存

比如说下面这个 Person 类,包含成员变量 char* 用于存储名字,是一个字符数组

class Person { 
public: 
 Person(const char *s); 
 ~Person(); 
 void print(); 
 // ... accessor functions 
private: 
 char *name; // char * instead of string 
 //... more info e.g. age, address, phone 
};

如果采用的是默认的拷贝构造函数,就会出现新拷贝的和原先拷贝的对象中 char* name 指向同一块数据的情况,当一个对象调用析构函数释放掉相应的资源后,另一个对象再释放的时候就会发现相应的数据已经被释放,于是出现错误的情况。这时候我们就需要自己写一份拷贝构造函数,深拷贝字符串的内容

Person::Person( const Person& w ) { 
 name = new char[::strlen(w.name) + 1]; 
 ::strcpy(name, w.name); 
}

但是如果是利用 String 来存储名字的信息,那么默认的构造函数可以很好地完成深拷贝

#include <string>
class Person { 
public: 
 Person( const string& ); 
 ~Person(); 
 void print(); 
 // ... other accessors ... 
private: 
 string name; // embedded object (composition) 
 // ... other data members... 
};

什么时候会调用拷贝构造函数?

  • Initialization
Person baby_a("Fred"); 
Person baby_b = baby_a; // not an assignment 
Person baby_c( baby_a ); // not an assignment
  • Call by value
void roster( Person ) {
 ...
}
Person child( "Ruby" ); // create object 
roster( child ); // call function
  • Function return
Person captain() {
 Person player("George");
 return player; 
}
Person who = captain();

Copies and overhead

Compilers can eliminate copies when it' s safe!

Person copy_func( Person p ) { 
 p.print(); 
 return p; // copy ctor called! 
} 
Person nocopy_func( char *who ) { 
 return Person( who ); 
} // no copy needed!

这是一种返回值优化

using vector<T> container

对于 vector 容器来说,每一次 push_back 都是一次拷贝,同时如果预先没有设置容器大小,容器的大小也会随着数目的增多做扩容,而扩容的时候也是拷贝。要避免这些拷贝,可以选择一开始通过 reserve 设置好容量,避免扩容造成的拷贝;而对于每一次插入所需要的拷贝,可以使用 emplace_back(Point(1,2)) 从而不用先创建一个临时变量再拷到容器中去了

拷贝构造函数和赋值运算符的区别

  • 拷贝构造函数是在对象被创建的时候调用的、
  • 赋值运算符只能使用于已经存在的对象,也就是进行赋值之前,这个对象已经被某个构造函数构造出来了

Rule of three

对于析构函数,拷贝构造函数,重载运算符=,这三个函数只要有一个需要重写,那么三个基本都要重写,比如说下面这三个函数

Person(const char *s){
        name=new char[strlen(s)+1];
        strcpy(name,s);
    }
~Person(){
    delete[] name;
}
Person& operator=(const Person& other){
    if (this != &other){
        delete[] name;
        name = new char [strlen(other.name)+1];
        strcpy(name,other.name);
    }
    return *this
}

注意这里重载运算符=函数如果只写为

Person& operator=(const Person& other){
    delete[] name;
    name = new char [strlen(other.name)+1];
    strcpy(name,other.name);
    return *this
}

那么当出现 p2=p2 的时候,即自己给自己赋值的时候,一开始就把 name 给删掉了,那么后面 strcpy 的时候就无法正确赋值了

如果我们希望禁用拷贝构造函数,在 C++ 11 以前,将拷贝构造函数声明为private且不提供实现;在C++ 11以后,声明函数 Person(const Person &rhs) = delete;即可

2.11 强制类型转换

static_cast

static_cast 用于数据类型的强制转换,有这样几种用法

  • 基本数据类型的转换,比如char转换成int
  • 在类的体系中把基类和派生类的指针和引用进行转换
  • 向上转换是安全的
  • 向下转换是不安全的
  • 只能在有相互联系的类型中进行相互转换,不一定包含虚函数
  • 把空指针转换成目标类型的空指针
  • 把任何类型转换成void类型

  • static_cast不能转换掉有const的变量

const_cast

const_cast 可以强制去掉const的常数特性,只能用在指针和引用上面

  • 常量指针被转化成非常量的指针,仍然指向原来的对象
  • 常量引用被转换成为非常量的引用,仍指向原来的对象

来看一段代码 - 打印出来的结果是a=10,而p和q所指向的值是20,a的地址和pq指向的地址是一样的 - 事实上第五行的赋值是一种未定义行为,最好别用

const int a = 10;
const int *p = &a;
int *q;
q = const_cast<int *>(p);
*q = 20;
cout << a << " " << *p << " " << *q << endl;
cout << &a << " " << p << " " << q << endl;

reinterpret_cast

reinterpret_cast 主要有三种用途

  • 改变指针或者引用的类型
  • 将指针或者引用转换成为足够长的整形
  • 将整型编程指针或者引用类型

dynamic_cast

跟其他几个不同,其他几个都是编译时完成的,dynamic_cast 是在运行时进行类型检查的

不能用于内置的基本数据类型的强制转换

如果成功的,将返回指向类的指针或者引用,转换失败的话会返回NULL

转换时基类一定要有虚函数,否则无法通过编译

  • 原因是虚函数表名这个类希望可以用基类指针指向派生类,这样转换才有意义

  • 在类的向上转换的时候,和static_cast 效果一样,但是向下转换的时候比static_cast 更安全,因此要求也更高

来看一段代码

int main()
{
    A a;
    B b;
    A *ap = &a;
    if(dynamic_cast<B*>(ap)) {
        cout << "OK1" << endl;
    }
    else {
    cout << "Fail" << endl;
    }
    if(static_cast<B*>(ap)) {
        cout << "OK2" << endl;
    }
    else {
        cout << "Fail" << endl;
    }
    ap = &b;
    if(dynamic_cast<B*>(ap)) {
        cout << "OK3" << endl;
    }
    else {
        cout << "Fail" << endl;
    }
    if(static_cast<B*>(ap)) {
        cout << "OK4" << endl;
    }
    else {
        cout << "Fail" << endl;
    }
    return 0;
}
  • 运行的结果是第一个失败,其他的都成功
  • 推测导致这个结果的原因:
    • 当ap指向派生类的时候,进行强制类型转换变成派生类是可以成功的
    • 当ap指向基类的时候,dynamic_cast 转换是否成功取决于指针指向的类型和即将转换的类型是不是一样,不一样就会失败,返回一个NULL,而static是可以成功的
    • 其实是更安全的机制导致dynamic的检查更多,要求更高

评论区

对你有帮助的话请给我个赞和 star => GitHub stars
欢迎跟我探讨!!!