五大cast转型

const_cast

  • const_cast是唯一能将const性质转化掉的操作符,但执行其他任何类型的转换都会引起编译错误。

单纯的把常量转为变量是没多少意义的,也就是这种代码:

1
2
const int con =12;
int cc= static_cast<int>(con);

直接用变量或者mutable就好了。

如果定义了一个非const的变量,却使用了一个指向const值的指针来指向它(不规范的风格),在程序的某处我们想改变这个变量的值了,但手头只持有指针,这是const_cast就可以用到了:

1
2
3
4
int constant = 26;
const int* const_p = &constant;
int* modifier = const_cast<int*>(const_p);
*modifier = 3;

这并不是一个好的设计,还是应该遵从这样的原则:使用const_cast去除const限定的目的绝对不是为了修改它的内容,只是出于无奈,所以最好少用转型。

如果有一个函数,它的形参是non-const类型变量,而且函数不会对实参的值进行改动,这时我们可以使用类型为const的变量来调用函数,也就用到const_cast了。

1
2
3
4
5
6
7
8
9
void InputInt(int * num)
{
cout<<*num<<endl;
}
int main()
{
const int constant = 21;
InputInt(const_cast<int*>(&constant));
}

dynamic_cast

dynamic_cast用于类继承层次间的 指针或引用 的转换。和qobject_cast类似,用于执行安全的向下转型

dynamic_cast 只能用于含有虚函数的类,虚函数表是是dynamic_cast操作符转换能够进行的前提条件。虚表的最前面是指向 type_info 的指针,包含了类的继承信息、类的描述等。能否转换成功,所依据的就是type_info如果转换失败,new_type 是指针,那么将返回NULL;如果 new_type 是引用,那么将抛出异常 std::bad_cast

向下转型有两种情况。一种是基类指针指向派生类的对象,这种转换是安全的;另一种是基类指针所指对象为基类类型,在这种情况下dynamic_cast在运行时做类型检查,转换失败,返回结果为0

最常用的情况是这样:

1
2
3
Base* bb = new Derive();
Derive* dd = dynamic_cast<Derive*>(bb);
dd->test();

如果没有虚函数,会报错: error: C2683: “dynamic_cast”:“Base”不是多态类型

dynamic_cast在基类和派生类指针之间转换时,会遍历整个继承体系进行类型检查,因此dynamic_cast时间和空间代价较高。比如4级的继承结构,dynamic_cast<Base>将会调用4次才能确定最终的那个子类型,尤其避免连续使用转型,用虚函数代替这种情况。

static_cast

类似于C风格的强制转换。无条件转换,静态类型转换。用于:

  • 子类转换为父类:其中子类指针转换成父类指针是安全的;但父类指针转换成子类指针是不安全的,此时用dynamic_cast

  • 基本数据类型转换,例如double, int, char, float等,不能进行无关类型(如非基类和子类)指针之间的转换。

  • void* 转换为某类型指针

1
2
double a = 1.999;
int b = static_cast<double>(a); //相当于a = b ;

大多数的编译器都会给出一个警告:从“double”转换到“int”,可能丢失数据。static_cast可以明确告诉编译器,这种损失精度的转换是在知情的情况下进行的

1
2
3
4
double a = 1.999;
void * vptr = & a;
double * dptr = static_cast<double*>(vptr);
cout<< *dptr <<endl; //输出 1.999

从这里能看出,static_cast进行的是简单粗暴的转换,static_cast不做运行时的类型检查以保证转换的安全性,所以static_cast不如dynamic_cast安全,其正确性完全由程序员自己保证。

reinterpret_cast

十分危险,平时不要使用。

  • 从底层对数据进行重新解释,依赖具体的平台,可移植性差

  • 可以在指针和引用随意的转换

  • 可以将整型转换为指针,也可以把指针转换为数组

qobject_cast

参考:qobject_cast 分析及QTBUG 20616


静态绑定和动态绑定

对于非虚成员函数,C++是静态绑定的,而虚函数都是动态绑定,如此才可实现多态性。这也是C++语言和其它语言Java, Python的一个显著区别。

几个名词定义:

  • 静态类型:对象在声明时采用的类型,在编译期既已确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,表现一个对象将会有什么行为,是在运行期决定的;
  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

看这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Base
{
public:
Base();
virtual ~Base();
void test();
void test_pub();
virtual void test_virtual();
private:
int m;
};

void Base::test()
{
cout<<"base test"<<endl;
}
void Base::test_pub()
{
m = 10;
cout<<"base test_pub"<<endl;
}
void Base::test_virtual()
{
cout<<"base virtual"<<endl;
}

调用:

1
2
3
4
Base *b = NULL;
b->test();
b->test_pub();
b->test_virtual();

如果对三个函数分别测试,会发现只有test()能正常运行,后两个函数运行时会等待一小段时间,然后就返回了。

指针为空,仍然能正常运行test函数,这是因为代码进行了静态绑定,在编译期就确定了。那为什么test_pub不行?因为涉及到了this指针,也就是说这两个函数实际是这样的:

1
2
3
4
5
6
7
8
9
void Base::test(Base* this)
{
cout<<"base test"<<endl;
}
void Base::test_pub(Base* this)
{
this->m = 10;
cout<<"base test_pub"<<endl;
}

如果this指针是NULL,当然无法操作成员变量,所以报错。在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址(即this指针)作为一个隐含参数传递给函数。说白了,test函数传入了一个错误的参数,但又没用到这个参数,所以可以运行。但如果是静态函数,那么编译器不会给它传this指针。

这里对成员函数的解析,和查找对应的代码都是在编译阶段完成的,这就是所谓的静态绑定。只要函数不涉及this指针,空类指针也可以调用函数,C++只关心指针类型,不关心指针指向的对象是否有效,C++要求程序员自己保证指针的有效性,这也是为什么规范的代码经常会有if(b!=NULL)这样的语句。

那么test_virtual为什么也不能正常运行?test_virtual是虚函数,对它的调用只能等到运行期才能确定,然后才发现是空指针,无法运行。

参考:C++中的静态绑定和动态绑定


strcat strcpy strlen strcmp

strcat

我自己写的是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
char* _strcat(char *_Dest, const char *_Source)
{
assert((_Dest != NULL) && (_Source != NULL));
char* p= _Dest;
while(*p!='\0')
{
p++;
}
const char* temp = _Source;
while( *temp!='\0')
{
*p=*temp;
p++;
temp++;
}
return _Dest;
}

strcat的源码如下:
1
2
3
4
5
6
7
8
9
10
char *strcat(char *des, const char *src)
{
assert((des!=NULL) && (src!=NULL));
char* address = des;
while(*des != '\0') // 移动到字符串末尾
++des;
while(*des++ = *src++) //dest的下一个字符用src的下一个字符赋值
;
return address;
}

必须加assert判断,src指向的字符添加到dest所指向的结尾处,覆盖原来的\0,添加新的\0。二者所指的内存区不可重叠,并且dest要有足够的内存容纳src字符串,但源码也没有对此判断,所以它是危险的。注意其中dest不能指向常量区。

这样用是错的,p1指向了常量字符串:

1
2
3
char *p1 = "123";
char *p2 = "ABC";
std::cout<<strcat(p1,p2)<<endl;

改变连接的起始点:

1
2
3
char p1[4]="123";
char p2[4]="ABC";
std::cout<<strcat(p1+2,p2)<<endl;

结果是3ABC

以’\0’作为连接的标志:

1
2
3
char p1[40]="123\0abc";
char p2[8]="ABC\0DEF";
std::cout<<strcat(p1,p2)<<endl;

结果是123ABC

strcpy

函数源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
char* _strcpy(char *dst, const char *src)
{
assert((dst!=NULL)&&(src!=NULL));
char* p1 =dst;
const char* p2 = src;
while(*p1!='\0')
{
*p1 = *p2;
p1++;
p2++;
}
return dst;
}

函数不安全,src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。
源指针所指的字符串内容是不能修改的,因此应该声明为 const 类型。与strcat相比,没有先移动到dst末尾。注意复制src到\0为止。

测试代码:

1
2
3
char s1[]="12345";
char s2[]="abc";
std::cout<<strcpy(s1,s2)<<endl; //abc

复制s2到s1时,以\0为准。

strlen

我写的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
int _strlen(const char* s)
{
assert(s!=NULL);
char* p =s;
int n=0;
while(*p!='\0')
{
p++;
n++;
}
return n;
}

注意指针不能为NULL,不统计最后的’\0’

strcmp

两个字符串的大小不能用关系运算符判断,应该用strcmp(a,b),两个字符串相同时返回0,a>b返回一个正值,否则返回负值.
从左向右逐个比较a,b中的字符,根据二者的ASCII值确定结果,如果二者一直相同,但字符串a长度较小,那么结果为负。
我用的编译器对正负的处理是1和-1,有的是ASCII的差值。
我是这样实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int _strcmp(const char *dst, const char *src)
{
assert((dst!=NULL)&&(src!=NULL));
const char* p1 = dst;
const char* p2 = src;
while(*p1!='\0' && *p2!='\0')
{
if(*p1 > *p2)
return 1;
else if(*p1 < *p2)
return -1;
p1++;
p2++;
}
if(*p1!='\0' && *p2=='\0')
return 1;
else if(*p1=='\0' && *p2!='\0')
return -1;
return 0;
}

一种源码是这样的:

1
2
3
4
5
6
7
8
9
int strcmp(const char *s1, const char *s2)  
{
//两字符串相等的情况,如果两个字符串中所有的字符相等,则返回0
while (*s1 == *s2++) //两个字符相等
if (*s1++ == 0) //达到字符串末尾
return (0);
//不相等,则返回值为第一次不相等时的两字符之差
return (*(unsigned char *)s1 - *(unsigned char *)--s2);
}

以上几个函数中的src全要加const限定,strcmp的两个参数都要加const

参考:


深入探究虚函数 (一)

在实现C++多态时会用到虚函数。使用虚函数目的是通过基类指针访问派生类定义的函数。

典型使用:

1
2
Base* ba = new Derive();
Derive *de = ba;

反射性地会说ba是个基类指针,指向派生类对象。但是运行程序竟然报错了: error: C2440: “初始化”: 无法从Base * 转换为 Derive * 从基类型到派生类型的强制转换需要 dynamic_cast 或 static_cast 。也就是需要向下转型,为什么?ba不是指向派生类对象吗?

这就是虚函数的动态绑定, 在运行期才指向派生类对象,而在编译期还是基类指针,不能赋给de,语法检查就过不了,当然报错了。

我们一般说ba的静态类型是Base*,动态类型是Derive*。如果第一行改为Base* ba,那么静态类型是Base*,没有动态类型。

  • 所谓静态多态是指通过模板或者函数重载实现的多态,其在编译期确定行为。动态多态是指通过虚函数技术实现在运行期动态绑定的技术。

  • 普通函数在编译时就确定了函数地址,但虚函数在运行时查询虚函数表,所以效率较低。

  • 要正确实现虚函数,只能通过基类的指针或引用来指向派生类对象

  • 派生类重新实现基类中的虚函数,这就叫覆盖

  • 成员虚函数不能有同名同参数的静态函数

  • 多个虚函数对sizeof的计算相当于一个,因为它们都存在虚函数表里,虚函数表只对应一个虚指针,所以不会扩大占用内存

纯虚函数

基类有纯虚函数时,就是抽象类,抽象类无法实例化,也无法声明指针。派生类可以不实现这个纯虚函数,但这样就没有意义了。 基类中的纯虚函数其实也可以实现,但没有必要,因为派生类都会重新实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
virtual void testPureVirtual() = 0;	 // 基类中声明纯虚函数

void Base::testPureVirtual() // 基类中实现纯虚函数
{
cout<<"base pure virtual test "<<endl;
}

void Derived::testPureVirtual() // 派生类中实现虚函数
{
Base::testPureVirtual();
cout<<"derived pure virtual test"<<endl;
}
Derived d;
d.testPureVirtual();

这里的代码在派生类中调用了基类中的纯虚函数,由于抽象类无法实例化,所以基类纯虚函数是不能直接调用的。

不要重新定义继承而来的虚函数的默认参数值

绝对不要重新定义一个继承而来的virtual函数的缺省参数值,因为缺省参数值都是静态绑定(为了执行效率),而virtual函数却是动态绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base
{
public:
Base();
virtual ~Base();
virtual void test_virtual(int i=0); // 默认值为0
};
void Base::test_virtual(int i)
{
cout<<"base virtual "<<i<<endl;
}

class Derive : public Base
{
public:
Derive();
void test_virtual(int i=1); // 默认值改为1
};
void Derive::test_virtual(int i)
{
cout<<"derive virtual "<<i<<endl;
}

还是那个常用的调用:

1
2
Base* ba = new Derive();
ba->test_virtual();

结果竟然是:
1
derive virtual  0   // 函数为派生类的,默认值为基类的

原因在于虚函数是动态绑定的,但参数是静态绑定的, 覆盖虚函数时只能覆盖动态绑定的部分,所以不能改变参数。虚函数的效率较差,如果对参数也进行动态绑定,那么对执行速度和编译器简易度不利,所以C++做了分开的处理。

不能声明为虚函数的函数

构造函数

首先明确一点,在编译期,编译器完成了虚表的创建,而虚指针在构造函数期间被初始化

如果构造函数是虚函数,那必然需要通过虚指针来找到虚构造函数的入口地址,但是这个时候我们还没有把虚指针初始化。因此,构造函数不能是虚函数。

內联函数

编译期內联函数在调用处被展开,而虚函数在运行时才能被确定具体调用哪个类的虚函数。內联函数体现的是编译期机制,而虚函数体现的是运行期机制。 二者不是同一范畴的东西。

静态成员函数

静态成员函数和类有关,即使没有生成一个实例对象,也可以调用类的静态成员函数。而虚函数的调用和虚指针有关,虚指针存在于一个类的实例对象中,如果静态成员函数被声明成虚函数,那么调用成员静态函数时又如何访问虚指针呢。也就是说,静态成员函数与类有关,而虚函数与类的实例对象有关。 二者也不是同一范畴的东西。

多态

多态存在的3个必要条件:

  1. 继承
  2. 函数的重写
  3. 父类引用或指针指向子类对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class B0    //基类BO声明
{
public: //外部接口
virtual void display() //虚成员函数
{
cout << "B0 display0" << endl;
}
};
class B1 :public B0
{
public:
void display() { cout << "B1 display0" << endl; }
};

// 尝试形参改为 B0& ptr 或 B0* ptr
void fun(B0 ptr) //普通函数
{
ptr.display();
}
B0 b0; //声明基类对象
B1 b1; //声明派生类对象
fun(b0); //调用基类B0函数成员
fun(b1); //调用派生类B1函数成员

运行结果是B0 display0 B0 display0
虚函数的动态绑定仅在 基类指针或引用 绑定派生类对象时发生,fun的形参不是指针而是对象,所以调用哪个版本的函数在编译时就已经确定。

参考:
C++虚函数表剖析
深入C++对象模型&虚函数表


C++中protected访问权限问题

今天发现有这样两句话:

  1. 基类的保护成员对于派生类的成员是可访问的。
  2. 派生类的成员只能通过派生类对象访问基类的保护成员,派生类对一个基类对象中的受保护成员没有访问权限。

这两句话看的太头晕了,其实作者应该是想表达:只有在派生类体内,才可以通过派生类对象,访问基类的protected成员。看这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base
{
public:
Base();
private:
void foo();
protected:
int m;
};

class Derive : public Base
{
public:
Derive();
};
Derive::Derive()
{
this->m = 1; // 这里访问m没有问题
}

使用类的代码:

1
2
Derive *d = new Derive();
std::cout<< d->m <<endl; // 这里就报错了

总结一下三个关键字的访问范围:

  • private: 只能被类成员函数及友元访问,不能被其他任何访问,本身的类对象也不行。
  • protected: 只能被类成员函数、子类函数及友元访问,不能被其他任何访问,本身的类对象也不行 也就是说protected比private多了个子类函数
  • public: 以上所说全能访问

还是上面的两个类,只看public和private,如下代码:

1
2
3
4
Base *b = new Base();
b->test_pub(); // 通过
b->foo(); // 报错
std::cout<< b->m; // 报错

protected继承和private继承能降低访问权限:

  • 使用private继承,父类的protected和public属性在子类中变为private

  • 使用protected继承,父类的protected和public属性在子类中变为protected

  • 使用public继承,父类中的protected和public属性不发生改变


构造函数与析构函数
  • 构造函数和析构函数不能被继承。正因为如此,派生类对象会先调用基类的构造函数,而且只能调用这一次,析构函数也是同理。

  • 如果基类定义了带参数的构造函数,派生类没有定义任何带参数的构造函数,则不能直接调用基类的带参构造函数,程序编译失败。

  • 类的对象声明会调用构造函数,但类的指针不会。例如MyClass p1, *p2;中p1调用构造函数,p2不会。

  • 如果类A中的成员变量有类B的对象,那么声明A对象时,先运行B的构造,再运行A的构造。不过一般用B的指针比较好

  • 构造函数中的成员列表初始化顺序取决于类中声明的顺序;析构函数销毁数据成员的顺序与声明顺序相反

  • 构造函数不能是虚函数。类有一个指向虚函数表的指针用于调用虚函数,这个指针是在构造函数里初始化的,如果构造函数是虚函数,怎么在没有初始化的情况下调用它?

  • 构造函数中最好不要调用虚函数,某些编译器会报错。因为先运行基类构造函数,再运行派生类的。基类的构造函数时已经出现了虚函数表指针,它指向基类的虚函数表,所以基类的构造函数中调用的虚函数是基类的。这样就无法实现多态了,所以没有意义。参考Effective C++条款 9



  • 如果需要手动实现一个析构函数,通常意味着类实例中有动态分配的内存空间,只有这种情况,需要程序员手动释放分配的内存空间。 同时也需要实现拷贝构造函数和重载赋值运算符。

  • 析构函数无返回类型,只能有一个,也就不能重载。

  • 继承体系中,析构函数执行的顺序: 派生类析构 —- 类成员的析构 —- 基类析构

函数的局部对象是逆序销毁的

1
2
3
4
5
6
int main()
{
Foo f;
Base b;
return 0;
}

运行结果为:

1
2
3
4
Foo construct 
base constructor
base destructor
Foo destruct

构造函数不难理解,当然按顺序,析构是逆序执行的。看完这个例子就会发现,派生类对象在内存中的生存销毁和它很相似:基类构造——派生类构造——派生类析构——基类析构,相当于先有个基类对象,再有派生类对象

更广泛的结论:

  • 全局对象或全局静态对象不管在什么位置定义,它的构造函数都在main()函数之前执行
  • 所有在stack上的对象都比在全局或静态对象早销毁。
  • 不管是在栈上的对象,还是全局或静态对象,都遵循这样的顺序: 越是先产生的对象越是后被销毁

虚析构函数

假如一个类没有派生类,那么析构函数可以不是虚函数。 如果有派生类,析构函数要声明为虚函数,这是为了防止新手犯错误。如果基类指针指向了派生类的对象,析构时,只会调用基类析构函数,没有派生类的析构函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base
{
public:
Base(){ }
~Base(){ // 应加上virtual声明
qDebug()<<"Base destructor";
}
};

class Derived:public Base
{
public: Derived(){ }
~Derived(){
qDebug()<<"Derived destructor";
}
};

Base* p = new Derived();
delete p;

运行结果是Base destructor。给Base类析构函数加上virtual声明后,运行结果:
1
2
Derived destructor
Base destructor

一开始我没加virtual,仍然调用了派生类的析构函数,后来发现是基类习惯性继承了QObject类,而后者的析构函数是virtual声明的。于是有结论: 对于继承树,只要在根基类上声明虚析构函数就可以,所有的子类的析构函数也就是虚的。

如果析构函数是虚函数,运行时会动态绑定到派生类的析构函数.如果不是虚函数,想安全删除pBase必须delete (Derived*)p. 

可能有人要问了,为什么C++不直接把虚析构函数作为默认值?原因是虚函数表的开销以及和C语言的类型的兼容性。有虚函数的对象增加一个指针

不能随意声明虚析构函数,注意以下原则:

  • 类至少包含一个虚函数的时候,需要虚的析构函数,因为它必然有派生类
  • 类有派生类时,需要虚的析构函数,否则不需要

补充 override关键字

C++11新特性中的关键字override,编译器会检查基类中的虚函数和派生类中带有override的虚函数有没有相同的函数签名,一旦不匹配便会报错。 因此在子类析构函数后增加关键字override,一旦父类缺少关键字virtual就会被编译器发现并报错。 override 要求必须是虚函数且父类的虚函数必须有virtual关键字,函数的参数列表和返回值也必须相同。

1
2
3
4
5
6
7
8
9
10
11
12
class Base
{
public:
virtual ~Base() { ::printf("base\n"); }
};

class Derived
: public Base
{
public:
~Derived() override { ::printf("derived\n"); }
};


Sublime

取消自动补全括号引号

1
"auto_match_enabled": "false",

修改 minimap 的颜色

Settings的修改:

1
2
"always_show_minimap_viewport": true,
"draw_minimap_border": true,

.sublime-theme文件部分的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"class": "minimap_control",
"settings": ["always_show_minimap_viewport"],
"viewport_color": [0,191,255],
"viewport_opacity": 0.5,
},

{
"class": "minimap_control",
"settings": ["always_show_minimap_viewport"],
"viewport_color": [0,191,255],
"viewport_opacity": 0.5,
"viewport_opacity": { "target": 0.4, "speed": 3.0, "interpolation": "smoothstep" },
},
{
"class": "minimap_control",
"attributes": ["hover"],
"viewport_color": [0,191,255],
"viewport_opacity": 0.5,
"settings": ["always_show_minimap_viewport"],
"viewport_opacity": { "target": 0.4, "speed": 12.0, "interpolation": "smoothstep" },
},

高级搜索

搜索范例.png
其中的含义:

1
2
3
4
5
6
7
8
*.cs
表示找 cs 后缀的文件,也可以写多个后缀

-/*Debug/*
表示排除 Debug 文件夹内的所有文件

-*.cache
表示排除 cache 后缀的文件, -test.c是不在test.c中搜索

修改sublime标签的颜色


using关键字

引入命名空间,一个 using 声明一次只引入一个命名空间,这个不用讲了

使用class有时可能会遇到这种情况:基类中的函数func有多个重载版本,但是派生类只对版本1进行了重定义。 结果派生类 隐藏 了基类中所有版本,通过派生类只能访问重定义的版本1,不能再访问基本的版本2,3,4,则派生类要么重定义所有重载版本,或者一个也不重定义。 如果基类中重载版本太多,在派生类中全重定义一遍就太麻烦了。

这就是using关键字适用的场合,可以在派生类中为重载成员名称提供 using 声明,使派生类使用的全是基类的函数版本。在此基础上,再重定义需要的那些函数,其他版本仍是基类的版本。

说的太抽象了,看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base
{
public:
Base() {}
virtual ~Base() {}

virtual void func(int i) { std::cout << "func in base " <<i<< endl;}
virtual void func(float i) { std::cout << "func in base " <<i<< endl;}
};

class Derived : public Base
{
public:
Derived() {}
// using Base::func;
virtual void func(int i) { std::cout << "func in derived " <<i*5<< endl;}
};

Derived d;
d.func(12);
d.func(float(12.20));

不加using时,运行结果是:

1
2
func in derived 60
func in derived 60

也就是对于d.func(float(12.20));,派生类仍然调用了自己的版本,但是我们实际上没有定义float参数的虚函数,这并不是我们想要的。

using后,只有参数是int的重载版本是派生类的,派生类没有定义虚函数的情况,还是调用基类的。运行结果是:

1
2
func in derived 60
func in base 12.2

隐藏对虚函数和非虚函数都是适用的,因为Effective C++说不要在派生类重新定义基类的非虚函数,所以这里我定义成了虚函数,由于多个虚函数对sizeof的计算相当于一个,所以不会扩大占用内存,可以仍按照Effective C++说的声明为虚函数。

using 用做 alias

1
2
3
using Type = std::vector<int>;

Type v={1,2,3,4,5,6};

再探默认构造函数

今天使用派生类时又发现了一个问题,基类和派生类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base
{
public:
Base(int a)
{ }
};

class Derived : public Base
{
public:
Derived()
{ }
};

还没编译,Creator就已经报错了
2
编译时Derived类会报错: error: no matching function for call to ‘Base::Base()’

这个问题的实质还是默认构造函数。我们都知道,如果一个类没有构造函数,编译器会为我们自动创建一个默认构造函数,这个函数没有参数,什么都没做。但是,当我们实现一个构造函数之后,编译器就不会创建,因此Base没有默认构造函数。

默认构造函数就是在调用时不需要explicitly传入实参的构造函数

上面我定义的Base(int a)是构造函数,而不是默认构造函数。 C++中,最多有一个默认构造函数,刻意是编译器生成,也可以是我们自己定义的。我们自己定义的默认构造函数只能是两种:

  1. 无参 2. 有参数,但参数都有默认值

比如下面两种都是默认构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base
{
public:
Base()
{ }
};
// 或者有默认值的有参构造函数
class Base
{
public:
Base(int a=0)
{ }
};

但是不要把这两种构造函数都放入类里,因为最多只能有一个默认构造函数。不实例化不报错,一旦实例化就会报错: 指定了多个默认构造函数,对重载函数调用不明确

派生类构造函数对基类初始化

再回到上面的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base{
public:
Base(int value){}
};

class Derived : public Base
{
public:
Derived()
{ }
private:
int m_value;
};

现在加入派生类,以上的代码会报错: error: constructor for ‘Derived’ must explicitly initialize the base class ‘Base’ which does not have a default constructor

如果基类没有默认构造函数,那么编译器也不会为派生类隐式地定义默认构造函数, 就会出现上面的错误。对这种错误,有两种修改方法:

  1. 对于有参的构造函数,参数赋默认值。也就是改成默认构造函数:把Base(int a)改为Base(int a=0)

  2. 最常用,也是Qt中所用的:在派生类的构造函数后,用列表初始化的方式调用基类构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Derived : public Base
    {
    public:
    Derived():
    Base(m_value)
    { }
    private:
    int m_value;
    };

上面的问题,可以总结如下:

  • 基类没有显式声明构造函数或者有一个无参数的构造函数,派生类构造函数可以不用对基类初始化,即忽略基类的构造函数
  • 基类的构造函数全是有参数的,派生类必须至少实现一个基类的构造函数,例如Qt中常见的:
1
2
3
4
5
explicit MainWindow(QWidget* parent=0);
// QMainWindow构造函数都有参数
MainWindow::Mainwindow(QWidget* parent):
QMainWindow(parent),
ui(new Ui::MainWindow)

这种方式解决的问题是:使用派生类创建一个对象后,怎样初始化从基类中继承过来的数据成员?(基类的构造函数是不能被继承的)

这种代码的具体格式:

1
2
3
4
派生类::派生类构造函数(总参数列表):基类构造函数(参数列表)
{
派生类中的数据成员初始化;
}

注意:如果没有基类和派生的关系,就不能用这种初始化格式,否则报错。

C++11中的default关键字

之前说了,如果我们显式声明构造函数,编译器就不会生成默认构造函数。 但是有时候,我们反而需要一个默认构造函数,比如下面的情况:

1
2
3
4
5
6
7
8
9
10
11
12
class C
{
public:
C(int f)
{
qDebug()<<"construct" ;
}
int a;
long b;
};

C obj;

很显然会报错,因为不存在默认构造函数了。我们可以加一个构造函数:C()=default;,这样编译就通过了

但是以下用法都会报错:

1
2
Base(double value=0)=default;
Base(float value)=default;

這是因为= default只能被加在 沒有默认参数 的special member function后面,Special member function包含: Default constructor, Destructor, Copy constructor, Copy assignment, Move constructor, Move assignment


构造函数的成员初始化列表 (一)

但是C++11规定,类的声明中可以给非静态成员变量赋初值:

1
2
3
4
5
class Base
{
public:
int m = 3;
};

类中的const成员进行初始化必须用这种方式

1
2
3
4
5
6
7
8
9
10
11
12
class Father {
public:
explicit Father(int father):
f(father),
m(10)
{

}
private:
const int f;
const int m;
};

如果不用这种方式初始化,会编译报错:error: C2789:必须初始化常量限定类型的对象

引用成员变量必须在列表中初始化

比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base
{
public:
Base();
explicit Base(int a);
private:
int m=4; // 类体内赋值,只适用于C++11
const int abc;
int& f;
};

Base::Base(int a)
:abc(10), // 常量
f(m) //引用
{
}

如果没有在构造函数成员列表初始化,会报错error: uninitialized reference member in ‘int&’ ‘const int’ [-fpermissive]
也就是上面的1和2的情况

类中的引用必须用列表初始化的方式赋值,不能在类体内或构造函数中用=赋值,否则会报错:operator= is private

两个类没有继承关系,但是类B的对象是类A成员变量,而B的构造函数都是有参数的,而且参数没有默认值,此时必须在类A构造函数中对B初始化

举例:Father类跟上面一样,增加类MyClass如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass
{
public:
explicit MyClass()
:father(s)
{

}
private:
Father father; // 没有默认构造行为的成员
int s;
};
class Father
{
public:
Father(int a){cout<<"Father construct"<<endl;}
};

MyClass构造函数前,先运行Father的构造函数,所以需要对其初始化,否则报错。这里实际上用到了类的隐式转换。
只要Father类有一个函数是无参数的,那就不需要在MyClass类中对father显式初始化,让编译器自动完成即可。其实就是初始化Father构造函数的参数。

我们有更好的优化方法:把father改成指针类型,实现对此规则的优化,这也是Qt中的常见方法
。 因为定义在heap上的类的指针是不会运行构造函数的,类的对象要运行构造函数,然后用成员初始化列表的方法对指针初始化:

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass
{
public:
explicit MyClass()
:father(new Father(s))
{

}
private:
Father* father;
int s;
};

类似的,Qt中的ui指针所指对象的初始化可以放到构造函数里:

1
2
3
4
5
6
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
ui = new Ui::MainWindow;
ui->setupUi(this);
}

构造函数中初始化的顺序

初始化的顺序与列表中的参数顺序无关,而是由类中定义的顺序决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Base::Base(int a)
{
std::cout<<"constructor Base:"<<a<<endl;
}

class Derive : public Base
{
public:
Derive();
Base a;
Base b;
};
Derive::Derive():
b(8),
a(4)
{
}

Derive* d = new Derive();

运行结果是先a后b:
1
2
constructor Base:4
constructor Base:8

与初始化时a和b的顺序无关,只跟类Derive中的声明顺序有关。

静态变量不能用构造函数成员列表初始化

静态变量不属于某个对象,而是被类共有,假如构造函数能初始化,会造成变量值不同。编译时报错:error: C2438 无法通过构造函数初始化静态类数据