31 空类中的默认成员函数
编译器会提供以下8个默认成员函数
A(){}; // 缺省构造函数
A(const A &tmp){}; // 拷贝构造函数
~A(){}; // 析构函数
A &operator=(const A &tmp){}; // 赋值运算符
A *operator&() { return this; }; // 取址运算符
const A *operator&() const { return this; }; // 取址运算符(const 版本)
当一个类没有定义任何自己的版本的拷贝控制函数成员,且它的所有数据成员都能移动构造或移动赋值时,还会提供默认的移动构造和移动赋值函数。
32 有组合,多继承情况下的对象构造顺序
1 按声明的继承顺序(class C:public A, public B 先构造A再构造B) 调用父类的构造
在有虚继承和一般继承存在的情况下,优先虚继承例如class C: public B, virtual public A
则先调用A的构造函数,再调用B的构造函数。初始化列表中如果有写直接父类的参数
C(int x1,int x2):A(x1),B(x2)则按继承时的声明顺序调用父类相应参数的构造函数。如果没有写C()则调用父类的默认构造。 这里再提一下,在初始化列表中只能调用父/祖先类的构造函数,而不能对父/祖先的成员变量直接初始化(即使这个成员变量在当前类内是public可见的也不行)!!!
2 类里面有类成员对象,类成员对象的构造函数优先被调用;(也优先于该类本身的构造函数),这里的调用顺序就是之前说的,按照在类中从上到下的声明顺序调用对应构造,如果此对象在当前类的初始化列表中被初始化则调用相应的构造函数,没有在初始化列表中出现则调用默认构造函数
3 调用自己的构造函数
析构顺序与构顺序完全相反
class A
{
public:
A(){ cout<<“A”<<endl;}
virtual ~A(){ cout<<“~A”<<endl; }
};
class B: public A
{
public:
B(){ cout<<“B”<<endl;}
~B() {cout<<“~B”<<endl; }
private:
A a;
};
class C: public A, public B //类在派生表中的继承列表
{
public:
C() {cout<<“C”<<endl;}
~C() {cout<<“~C”<<endl; }
private:
B b;
public:
A a;
};
int main()
{
C * p = new C;
delete p;
system(“PAUSE”);
return 0;
}
结果和分析:
A //1 public A 父类A的构造函数
A //2 public B,而B又继承自A 父类B中A的构造函数
A //3 父类B中成员变量a初始化 (调用父类A的构造函数)
B //4 public B,父类B自身的初始化 (调用父类B的构造函数)
A //5 C中成员变量b 的构造
A
B
A //6 C中成员变量a的构造
C //7 C的构造函数最后调用 (finally ,-__-|||)
33 如何禁止一个类被实例化
1 delete所有默认构造,默认拷贝,默认移动
2 为类中添加一个纯虚函数,则类变为抽象类,无法实例化
3 将所有构造函数设为私有属性
34 实例化一个对象的过程
1 分配空间,全局、静态对象的空间在编译器就已经分配,局部变量的空间分配由编译器决定,在生成局部变量的代码添加为局部变量在栈上分配空间的指令,在生命周期结束添加回收栈空间的指令。存储在堆空间的对象,是在运行到对应代码进行内存分配,运行到对应代码回收空间
(以下具体内容可以参考32 构造顺序)
2 父类构造
3 给自己的虚表指针赋值
4 自己初始化,初始化列表执行的是数据成员的初始化过程
5 自己的构造函数的函数体,在函数体内执行一些赋值操作
34 友元函数作用以及使用场景
通过友元,友元函数,友元类可以访问此类中的私有成员和保护成员
1 普通函数定义为友元函数
class A
{
friend ostream &operator<<(ostream &_cout, const A &tmp); // 声明为类的友元函数
public:
A(int tmp) : var(tmp)
{
}
private:
int var;
};
ostream &operator<<(ostream &_cout, const A &tmp)
{
_cout << tmp.var;
return _cout;
}
2 友元类 友元类(B)中可以直接访问 本类(A)的保护和私有成员
class A
{
friend class B;// 在A中声明其友元类, 表示B类中可以直接访问A的保护和私有成员
public:
A() : var(10){}
A(int tmp) : var(tmp) {}
void fun()
{
cout << "fun():" << var << endl;
}
private:
int var;
};
class B
{
public:
B() {}
void fun()
{
cout << "fun():" << ex.var << endl; // B是A的友元类所以 在B类中可以直接访问类 A 中的私有成员
}
private:
A ex;
};
小tip 在继承体系中,友元关系不能被继承,只有被友元声明的类才有友元关系
35 编译时多态和运行时多态的区别
编译时多态:在程序编译过程中出现,发生在模板和函数重载中(泛型编程)。
运行时多态:在程序运行过程中出现,发生在继承体系中,是指通过父类(爷爷类,祖先类)的指针或引用访问子类中的虚函数。
36 如何让类无法被继承
1 用final关键字,该关键字修饰的类无法被继承
class Base final
{
};
class Derive: public Base{ // error: cannot derive from 'final' base 'Base' in derived type 'Derive'
};
2 友元+虚继承+私有构造
template <typename T>
class Base{
friend T;
private:
Base(){
cout << "base" << endl;
}
~Base(){}
};
虽然 Base 类构造函数和析构函数被声明为私有 private,在 B 类中,由于 B 是 Base 的友元,因此可以访问 Base 类构造函数,从而正常创建 B 类的对象;
这里一定要虚继承的原因是,直接由最低层次的派生类构造函数初始化虚基类。这是因为在菱形继承中,可能会存在对虚基类的多次初始化问题,为了避免出现该问题,在采用虚继承的时候,直接由最低层次的派生类构造函数直接负责虚基类类的构造。如果不加virtual的话,在构造函数的顺序中,每个类只负责自己的直接父类的初始化,所以还是可以生成对象的。加上了virtual之后,C直接负责Base类的构造,但是Base类的构造函数和析构函数都是private,C无法访问,所以不能生成对象。
class B:virtual public Base<B>{ //一定注意 必须是虚继承
public:
B(){
cout << "B" << endl;
}
};
创建 C 类的对象时,C 类的构造函数要负责 Base 类的构造,但是 Base 类的构造函数私有化了,C 类没有权限访问。因此,无法创建 C 类的对象, B 类是不能被继承的类。
class C:public B{
public:
C(){} // error: 'Base<T>::Base() [with T = B]' is private within this context
};
37 C++11中的nullptr和NULL
NULL:预处理变量,是一个宏,它的值是 0,定义在头文件
nullptr:C++ 11 中的关键字,是一种特殊类型的字面值,可以被转换成任意其他指针类型。
1 nullptr的类型是nullptr_t( typedef decltype(nullptr) nullptr_t; )是一种 指针类型
2 NULL本质上是0,以NULL为实参调用重载函数,将会造成二义性,编译器不知道到底应该调用指针版本的函数还是整形版本的函数。
因为 NULL 本质上是 0,在函数调用过程中,若出现函数重载并且传递的实参是 NULL,可能会出现,不知和哪一个函数匹配的情况;但是传递实参 nullptr 就不会出现这种情况。
void fun(char const *p)
{
cout << "fun(char const *p)" << endl;
}
void fun(int tmp)
{
cout << "fun(int tmp)" << endl;
}
int main()
{
fun(nullptr); // fun(char const *p)
/*
fun(NULL); // error: call of overloaded 'fun(NULL)' is ambiguous
*/
}
38 关于引用的一些小补充
指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。(是否可变)
指针本身在内存中占有内存空间,引用相当于变量的别名,引用是否占内存,取决于编译器的实现。
如果编译器用指针实现引用,那么它占内存。
如果编译器直接将引用替换为其所指的对象,则其不占内存(毕竟,替换掉之后,该引用实际就不存在了)。
顺便一提,你无法用 sizeof 得到引用所占内存的大小,sizeof 作用于引用时,你得到的是它对应的对象的大小。
指针可以为空,但是引用必须绑定对象。(是否可为空)
指针可以有多级,但是引用只能一级。(是否能为多级)
39 C++中的几种显示强制类型转换
xxx_cast<newType>(data)
class Complex{
public:
Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ }
public:
operator double() const { return m_real; } //类型转换函数
private:
double m_real;
double m_imag;
};
1 static_cast
只能用于良性转换,这样的转换风险较低,一般不会发生什么意外。
(1 用于基本数据类型的转换。例如 short 转 int、int 转 double、const 转非 const之类的向上转型(将表达较窄的类型 转到 表达更宽的类型,这种转换一定不会导致类型溢出的情况,较安全)
(2 void 指针和具体类型指针之间的相互转换,例如void 转int 、char 转void 等
(3 可以将任何类型的表达式转化成 void 类型
(4 对于有重载 类型转换operator函数的类,可用static_cast触发调用这个operator
例如Complex 转 double(调用类型转换operator函数) static_cast
(5 对于定义了 单参(注意只能是单参,这样才是一对一的类型转换关系)构造函数的类(参数类型是我们传入的变量的类型),将构造参数传入static_cast函数,static_cast函数直接调用相应的构造函数 例如double转Complex调用转换构造函数 static_cast
static_cast —-会根据模板类型调用_Tp的拷贝构造,移动构造 or 参数构造 ,或者引用类型转换(不调构造) 来完成对应的类型转换
特别地对于我们函数有定义拷贝构造函数的情况下Complex (const Complex & c)
const Complex cc(1.0);// 调用单参double构造
static_cast
static_cast通过拷贝构造完成了const 转非 const的操作
一个tips static_cast是将()中的表达式返回值作为单参构造函数的参数,比如下式 static_cast
(6 用于类层次之间的父类和子生类之间 指针或者引用 的转换(不要求必须包含虚函数,但必须是有相互联系的类),用static_cast进行上行转换(子类的指针或引用转换成父类表示)是安全的;进行下行转换(父类的指针或引用转换成子生类表示)由于没有动态类型检查,所以是不安全的(下面会有讲要用dynamic_cast 实现下行转换)
Base p1;
Derive p2 = new Derive();
//向上类型转换 子类指针转型为父类指针类型
p1 = dynamic_cast
static_cast 是“静态转换”的意思,也就是在编译期间转换(其实除了dynamic_cast是运行时转换,其他的所有强制转换都是编译时转换),转换失败的话会抛出一个编译错误。
2 const_cast
它用来去掉表达式的 const 修饰或 volatile 修饰。换句话说,const_cast 就是用来将 const/volatile 类型转换为非 const/volatile 类型。
强制去掉常量属性,不能用于去掉变量的常量性!!!!只能用于去除指针或引用的常量性,将常量指针转化为非常量指针或者将常量引用转化为非常量引用(注意:表达式的类型和要转化的类型是相同的)。
const int n = 100;
int p = const_cast<int>(&n);// &n用来获取 n 的地址,它的类型为const int ,必须使用 const_cast 转换为int 类型后才能赋值给 p
p = 234;
cout<<”n = “<
3 reinterpret_cast
reinterpret_cast 这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,非常简单粗暴,所以风险很高。改变指针或引用的类型、将指针或引用转换为一个足够长度的整型、将整型转化为指针或引用类型。
reinterpret_cast 可以认为是 static_cast 的一种补充,一些 static_cast 不能完成的转换,就可以用 reinterpret_cast 来完成,例如两个具体类型指针之间的转换、int 和指针之间的转换(有些编译器只允许 int 转指针,不允许反过来)。
class A{
public:
A(int a = 0, int b = 0): m_a(a), m_b(b){}
private:
int m_a;
int m_b;
};
//将100--int转为int* 荒诞和危险的事情,这样的转换方式不到万不得已的时候不要使用
int *p = reinterpret_cast<int*>(100);
//将 A* 转换为 int*
p = reinterpret_cast<int*>(new A(25, 96));
// *p为25==m_a,使用指针直接访问 private 成员刺穿了一个类的封装性
4 dynamic_cast
其他三种都是编译时完成的,动态类型转换是在程序运行时处理的,运行时会进行类型检查
只能用于父类或子类的指针或者引用对象的转换,转换成功返回指向类型的指针或引用,对于指针,如果转换失败将返回 NULL;对于引用,如果转换失败将抛出std::bad_cast异常。
;不能用于基本数据类型的转换
(1 向上转型时,只要待转换的两个类型之间存在继承关系(这些信息在编译期间就能确定),就一定能转换成功。因为向上转型始终是安全的,所以 dynamic_cast 不会进行任何运行期间的检查(向上转型 进行的也是编译器检查),这个时候的 dynamic_cast 和 static_cast 就没有什么区别了。
Base p1;
Derive p2 = new Derive();
//向上类型转换 子类指针转型为父类指针类型
p1 = dynamic_cast
// 上面这种行为就是up_cast即多态的前提
(2 向下转型父类指针类型强制转换为子类指针类型,前提必须是安全的,要借助 RTTI 进行检测,所有只有一部分能成功。
dynamic_cast 会在程序运行期间借助 RTTI 进行类型转换,这就要求基类必须包含虚函数(下例中A,B,C类内都有虚函数)
继承顺序为: (后代)D —> C —> B —> (祖先)A。pa 是A类型的指针,当 pa 指向 A 类型的对象时,向下转型失败,pa 不能dynamic_cast为<B>或
当使用 dynamic_cast 对指针进行类型转换时,程序会先找到该指针指向的对象,再根据对象找到当前类(指针指向的对象所属的类)的类型信息,并从此类型开始沿着继承链向上遍历(从后代到祖先的顺序,就像我们上面写的那样),如果找到了要转化的目标类型,那么说明这种转换是安全的,就能够转换成功,如果没有找到要转换的目标类型,那么说明这种转换存在较大的风险,就不能转换。
上例当pa指向对象的所属类为A,当程序从这个类开始向上遍历时,发现 A 的上方没有要转换的 B 类型或 C 类型(实际上 A 的上方没有任何类型了),所以就转换败了。pa 指向 D 类对象,根据该对象找到的就是 D 的类型信息,程序从这个节点向上遍历的过程中,发现了 C 类型和 B 类型,所以就转换成功了。
对于同一个指针(例如 pa),它指向的对象不同,会导致遍历继承链的起点不一样,途中能够匹配到的类型也不一样,所以相同的类型转换产生了不同的结果。从表面上看起来 dynamic_cast 确实能够向下转型,本例也很好地证明了这一点:B 和 C 都是 A 的派生类,我们成功地将 pa 从 A 类型指针转换成了 B 和 C 类型指针。但是从本质上讲,dynamic_cast 还是只允许向上转型,因为它只会向上遍历继承链。
40 typeid运算符:获取类型信息
typeid 会把获取到的类型信息保存到一个 type_info 类型的对象里面,并返回该对象的常引用;当需要具体的类型信息时,可以通过成员函数来提取。
//获取一个对象的类型信息
Base obj;
const type_info &objInfo = typeid(obj);
cout<<objInfo.name()<<" | "<<objInfo.raw_name()<<" | "<<objInfo.hash_code()<<endl;
//class Base | .?AVBase@@ | 1035034353
//获取一个类的类型信息
const type_info &baseInfo = typeid(Base);
cout<<baseInfo.name()<<" | "<<baseInfo.raw_name()<<" | "<<baseInfo.hash_code()<<endl;
//class Base | .?AVBase@@ | 1035034353
name() 用来返回类型的名称
raw_name() 用来返回名字编码(Name Mangling)算法产生的新名称。raw_name() 是 VC/VS 独有的一个成员函数
hash_code() 用来返回当前类型对应的 hash 值, hash 值有赖于编译器的实现,在不同的编译器下可能会有不同的整数,但它们都能唯一地标识某个类型
typeid 运算符经常被用来判断两个类型是否相等。
编译器不会为所有的类型创建 type_info 对象,只会为使用了 typeid 运算符的类型创建。不过有一种特殊情况,就是带虚函数的类(包括继承来的),不管有没有使用 typeid 运算符,编译器都会为带虚函数的类创建 type_info 对象
class Base{};
class Derived: public Base{};
Base obj1;
Base *p1;
Derived obj2;
Derived *p2 = new Derived;
p1 = p2;
// typeid(p1) == typeid(Base*) 即typeid只考虑静态类型,不会考虑动态类型