C++基础知识、面试问题整理汇总
一、new和malloc的区别
- new与delete对应,删除数组时使用delete[],new返回一个指向新分配内存首地址的指针。new和delete等实际上是调用类的构造函数或者析构函数,从本质上也调用了malloc和free等;
- malloc和free是C/C++语言的标准库函数,new和delete是C++的操作符;
- 对于非内部数据类型的对象而言,无法使用C/C++标准库函数malloc和free,只能使用new和delete等调用类的构造函数或者析构函数;
- new申请的内存保存在堆中,malloc申请的内存保存在自由存储区(见第十点);
- 除此以外,malloc函数返回void指针,需要进行强制类型转换。
二、delete和delete[]的区别
- delete删除一个指针,delete[]删除一个数组;
- delete<->new, delete[]<->new[];
- 对于内建简单数据类型,delete和delete[]可以混用,对于自定义的复杂数据类型,delete和delete[]不能互用;
- delete[]会调用数组元素的析构函数,因为内建数据类型没有析构函数,因此问题不大;
- 但是对于复杂数据类型而言,如果对于其数组的删除只用delete的话,那么将只会删除一个指针,而不是原应调用的数组元素的析构函数。
三、C++基础运算符
名称 | 符号 |
---|---|
取模运算符 | % |
逻辑否、与、或 | !、&&、|| |
按位与、或、非 | &、|、~ |
按位移 | <<、>> |
四、char字符数组初始化以长度
可以用以下任一方法初始化字符串(字符数组):
char mystring[] = {'H', 'e', 'l', 'l', 'o', '/0'};
char mystring[] = "Hello";
两种模式下字符串(字符数组)的sizeof都是6,但是strlen都是5。第一种方式需要手动加上‘/0’,第二种方式由于使用了双引号“hello”来定义字符串,‘/0’是自动添加上的。
五、函数的若干问题
-
函数重载(Overloaded functions)
两个或多个函数可以用相同的名字,只要它们的参数的原型不同,也就是说可以用同一个名字定义多个函数,根据参数原型的不同进行不同的处理。 -
内联函数
inline指令在其他程序块内部放在函数声明之前。编译时,函数体的代码被放在符号表中,在调用的地方用函数体进行替换,从而节省函数调用中涉及的参数压栈传递、控制转移等的开销。内联函数是一个真正的函数,编译器在调用一个内联函数时,同样需要检查其参数的正确性。
六、类的private/protected/public属性
- 类的成员如果没有指定访问域,默认为private类型;
- 三种继承方式的区别在于基类的成员在继承类中访问属性的不同改变方式。
- public继承方式:基类成员的访问属性在派生类中保持不变(除了private类型以外);
private继承方式:将除了private以外的基类成员在继承类中访问属性改为private模式;
protected继承方式:将除了private以外的基类成员在继承类中改为protected模式; - 三种模式下都不能直接访问基类的private成员;
- 对于继承类的对象而言,只有public方式继承的派生类的对象才可以访问基类的public成员,这是因为在public继承模式下,基类的public成员的属性得以保存位public,而在private和protected继承模式下,基类public成员属性被修改为private或者protected,都不能被实例化的对象直接访问;
七、protected成员的特点和作用
- 对建立其所在类对象的模块来说,其访问性质与private成员一样,都不可以被对象直接访问;
- 对于其派生类来说,它的性质又和public成员性质相同,不管是public、private还是protected继承方式,在派生类中都可以和public成员一样被访问到;
- 既实现了数据隐藏,又方便了继承,实现代码重用。
八、关于空类
编译器为一个空类提供哪些默认函数?
- 默认的构造函数
- 默认的析构函数
- 拷贝构造函数
- 拷贝赋值操作符
两个默认的构造函数只有在没有其他构造函数被明确定义的情况下才存在。
一个类包含一个对赋值操作符assignatin operator(=)的默认定义,该操作符用于两个同类对象之间,并将参数对象(符号右边的对象)的所有非静态数据成员复制给其左边的对象。
sizeof()一个空类返回1。所谓类的实例化就是在内存中分配一块独一无二的地址。对于空类来说,其实例化也是如此,尽管空类并没有明确定义类成员,但是C++编译器对于空类实例化也会为其隐含添加一个字节的缺省成员,也就是长度唯一。因此sizeof一个空类为1。
九、继承或者多重继承下构造、析构函数调用顺序
具体看两个程序段:
1.派生类构造函数调用顺序
#include <iostream>
using namespace std;
class Base1 {//基类Base1,构造函数有参数
public:
Base1(int i)
{
cout << "Constructing Base1 " << i << endl;
}
};
class Base2 {//基类Base2,构造函数有参数
public:
Base2(int j)
{
cout << "Constructing Base2 " << j << endl;
}
};
class Base3 {//基类Base3,构造函数无参数
public:
Base3()
{
cout << "Constructing Base3 *" << endl;
}
};
class Derived : public Base2, public Base1, public Base3 {
public:
Derived(int a, int b, int c, int d) : Base1(a), member2(d), member1(c), Base2(b)
//此处的次序与构造函数的执行次序无关
{ }
private:
Base1 member1;
Base2 member2;
Base3 member3;
};
int main() {
Derived obj(1, 2, 3, 4);
return 0;
}
执行结果:
Constructing Base2 2
Constructing Base1 1
Constructing Base3 *
Constructing Base1 3
Constructing Base2 4
Constructing Base3 *
总结,上述情况构造函数执行顺序:
基类构造函数->成员涉及的构造函数->派生类自身的构造函数;
执行基类构造函数顺序:继承时被声明的顺序;
class Derived : public Base2, public Base1, public Base3 {
}
执行成员涉及的构造函数顺序:按照在类中定义的顺序,而不是初始化列表中的顺序;
Base1 member1;
Base2 member2;
Base3 member3;
十、堆和栈的区别
堆:堆是计算机科学中的一种特别的树状数据结构(完全二叉树)。其满足以下特性:给定堆中的任意节点P和C,如果P是C的父节点,那么P的值会小于等于(或大于等于)C的值。若P的值小于等于C的值,则称为最小堆;反之若P的值大于等于C的值,则称为最大堆。在堆中最顶端的那一个节点称为根节点。
栈:又称堆栈,其实和堆有区别。栈是计算机科学中的一种特殊的串列式的抽象数据类型。其特殊之处在于只允许在链表或者数组的一端进行加入数据的操作。与堆栈相对的数据结构称为队列。
从C++编译器为程序分配内存空间的机制来看,其为程序分配内存主要包括6种类型:
- 程序代码区:存放函数体的二进制代码,只读;
- 文字常量区:例如常量型字符串存放于该区域,程序执行结束之后系统自动释放;
- 全局(静态)区:C/C++的全局变量和静态变量被存放于该区域,区别在于C语言在位全局/静态变量分配存储空间时会考虑变量是否初始化,而C++不考虑;
- 自由存储区:C/C++中由malloc等函数分配的内存块存放于自由存储区之中,与堆具有一定的相似性,用free函数来释放;
- 堆:如前述。对于程序而言,由new分配的内存块被存放于堆中,并使用与new对应的delete运算符来释放。相对malloc分配的自由存储区而言,new分配的存放于堆中的内存更方便于管理,堆中的存储空间在程序执行结束以后自动回收;
- 栈:是提供给编译器自动分配、清楚并管理的内存空间。一般系统会为程序执行分配固定大小的栈空间(如1M)。像程序的作用域较小的局部变量、函数参数等就被存放于栈中。对于递归函数的实现来说,也可以使用基础的栈型结构来将递归实现为迭代。
堆和栈的区别整理如下表:
堆 | 栈 | |
---|---|---|
管理方式 | 程序员手动控制(new\delete等运算符) | 编译器自动管理 |
系统响应 | 系统维护记录空闲内存地址的链表,每次收到内存分配请求,系统遍历该链表寻找空间大于所需的堆结点并进行分配。多数系统还会在该内存空间首地址记录该分配的空间的大小,方便delete运算符进行释放 | 只要栈剩余空间大于程序所需,系统即为其分配,出现异常汇报stack overflow |
空间大小 | 堆在内存中是由链表维护的不连续的内存空间。堆的大小受限于系统中有效的虚拟内存大小(32位系统理论上是4G),内存空间大,灵活性高 | 栈是一块连续的内存区域,大小是OS预定,大小一般为1到2M |
碎片问题 | 堆结构如果涉及频繁的new和delete操作,将导致内存碎片化,影响程序执行效率 | 先进先出连续内存空间,因此不存在碎片问题 |
地址增长 | 堆由低地址向高地址增长 | 栈由高地址向低地址增长,执行/释放时从低地址的开始向高地址进行对应操作 |
分配方式 | 堆是动态分配,不可以通过静态分配 | 栈的分配方式具有动态分配和静态分配两种方式,其中动态分配由函数alloca实现。与堆不同的是,栈的动态分配方式由编译器自动执行,而不必像堆一样通过程序员delete函数等手动释放 |
分配效率 | 堆的分配由C/C++函数库提供,机制复杂,相对栈效率低 | 栈是由计算机硬件系统提供的数据结构,在硬件实现上提供了如寄存器等效率超高的结构存放栈地址,具有较高的效率 |
十一、引用和指针的区别
//使用指针
void swap(int* const pa, int* const pb){
int temp = *pa;
*pa = *pb;
*pb = temp;
}
int main(){
int a, b;
......
swap(&a, &b);
......
return 0;
}
//使用引用
void swap(int& a, int& b){
int temp = a;
a = b;
b = temp;
}
int main(){
int a, b;
......
swap(a, b);
......
return 0;
}
指针和引用的关系:
- 引用在底层通过指针来实现:一个引用对象,通过存储被引用对象的地址,来标识它所引用的对象
- 引用是对指针的包装,比指针更高级
- 指针是C语言就有的底层概念,涉及较多的地址操作,但是使用不好容易出错
- 引用隐藏了指针的“地址”的概念,不能直接进行地址操作,比指针更加安全
引用和指针的选择:
- 如果无需对地址进行操作,一般都可以用引用代替指针
- 可以用更多的引用代替指针,更加高效安全
- 引用的功能没有指针强大,有的时候不得不用指针,包括以下几种情况:
1.引用一经初始化,便无法更改被引用的对象,如果有这种需求,需要用指针;
2.没有空引用,但是有空指针;
3.函数指针(十三);
4.用new动态创建的对象或者数组,用指针存储其地址最为自然;
5.函数调用时,如需以数组形式传递大量数据,需要用指针作为参数。
十二、数组作为函数参数
- 数组元素作为实参,与单个变量一样
- 数组名作为参数,形参、实参都应该是数组名,且要保持类型一致,传送的是数组的首地址。并且对形参数组的修改会直接影响到实参数组
十三、指针函数和函数指针
指针函数:指针型函数,函数的返回类型是指针;
int *getNum(int a, int b);
函数指针:指向函数的指针;
int (*getNum)(int a, int b);
数据指针指向的是数据存储区,而函数指针指向的是程序代码存储区。
如下段代码:
#include <iostream>
using namespace std;
void printStuff(float) {
cout << "This is the print stuff function."
<< endl;
}
void printMessage(float data) {
cout << "The data to be listed is "
<< data << endl;
}
void printFloat(float data) {
cout << "The data to be printed is "
<< data << endl;
}
const float PI = 3.1415926F;
const float TWO_PI = PI * 2.0f;
int main(){
void (*functionPointer)(float);
printStuff(PI);
functionPointer = printStuff;
functionPointer(PI);
functionPointer = printMessage;
functionPointer(TWO_PI);
functionPointer(13.0);
functionPointer = printFloat;
functionPointer(PI);
printFloat(PI)
return 0;
}
上述程序段展示了三次函数指针调用,程序输出是:
This is the print stuff function.
This is the print stuff function.
The data to be listed is 6.28318
The data to be listed is 13
The data to be printed is 3.14159
The data to be printed is 3.14159
定义函数指针数组:
typedef void (*FuncPtr)();
void f1(){
cout << "f1" << endl;
}
void f2(){
cout << "f2" << endl;
}
void f3(){
cout << "f3" << endl;
}
FuncPtr f[] = {f1, f2, f3};
十四、this指针
- 隐含于每一个类的成员函数中的特殊指针;
- 明确地指出了成员函数当前所操作的数据所属的对象;
当通过一个实例化的对象调用类的成员函数时,系统会首先将该对象的地址赋值给类的this指针,相当于给this指针实际的意义。随后调用成员函数,当成员函数涉及对对象的数据成员进行操作时,就隐含地使用了this指针。
十五、浅拷贝与深拷贝
浅拷贝:实现对象间数据元素的一一对应复制,如一个对象指针复制给另一个对象指针,便仅仅将指针包含的地址进行复制,因此容易出现拷贝对象的成员随着原始对象成员一起变动的情况;
深拷贝:当被复制的对象数据成员是指针类型时,在复制时不是复制该指针成员本身,而是将该指针所指的对象进行复制。具体实现可以通过拷贝构造函数将对象的成员一一复制。
十六、字符数组和字符串类string
用字符数组表示字符串的缺点:
- 执行链接、拷贝、比较等操作,都需要显示地调用库函数,较为麻烦;
- 当不能确定字符串长度时,需要用new动态创建字符数组,最后需要用delete进行释放,较繁琐;
- 字符串实际长度不能长于为其分配的空间,否则会出现数组下标越界的错误。
string实际上是对字符数组操作的封装。
string类常用构造函数:
string();//缺省构造函数
string(const char *s);//使用指针s所指的字符串常量对string类进行对象初始化
string(const string &rhs);//拷贝构造函数,执行string对象的拷贝
注:C++中如“program”等字符串实际上类型是const char*类型,“program”在表达式中表示该字符串常量的首地址。
十七、cin和getline输入字符串
cin:使用cin操作输入字符串,会以空格作为分隔符,是空格后的内容会在下一回输入时被读取;
getline:存在与string头文件中,可以实现整行字符串输入读取,例如:
getline(cin, s2);//直接输入整行字符串
getline(cin, s2, ',')'//以','为分隔作为字符串结束的标志
十八、堆对象的管理
- 堆对象必须用delete进行删除:对于程序来所,new动态创建出来的对象被放在堆中,如果不及时进行释放,容易造成内存泄漏的问题;
- 明确每个堆对象的归属;
- 理想情况下,在一个类成员中创建的堆对象,也在这个类的成员函数中删除,也就是把对象的建立和删除事务变成一个类的局部问题;
- 如果需要在不同类之间转移堆对象的归属,一定要在注释中注明,作为类的对外接口约定,比如:
在一个函数内创建堆对象并将其返回,则该对象应该由调用该函数的类负责删除。
十九、STL容器
序列式容器:vector, deque(双端队列), list(双向链表)
关联式容器:set(内部由红黑树、平衡二叉树实现)(multiset), map(红黑树)(multimap)
链接:STL各容器简单介绍
二十、C++ stringstream的使用
C++引入了istringstream、ostringstream、stringstream三个类,要使用他们需要包含头文件sstream.h
#include <sstream.h>
//istringstream类用于执行C++风格的串流输入
//ostringstream类用于执行C风格的串流输出操作
//stringstream同时可以支持C风格的串流输入输出操作
三者继承关系如图所示:
二十一、7个字符字母常用函数
输入均为char类型,可以从字符串string截取
判断大小写:
- isupper()
- islower()
判断是否字母/数字: - isalpha()
- isdigit()
- isalnum() //是字母或者数字
转换大小写: - tolower()
- toupper()
二十二、STL中queue和stack没有clear()方法
queue是基于deque实现的,deque有clear方法,但是queue没有。
对于stack和queue数据的清空,方法如下:
stack<int> st;
stack<int>().swap(st);
或者自定义一个clear函数:
void clear(std::queue<int>& q){
std::queue<int> empty;
std::swap(q, empty);
}
二十四、数组的动态联编和静态联编
使用数组声明来创建数组时,将采用静态联编,即数组的长度在编译时设置;
使用new[]运算符创建数组时,将采用动态联编的方式(动态数组),即将在运行是为数组分配空间,其长度也将在运行时指定。使用这种动态数组以后,应使用delete[]释放其占用的内存。
二十三、虚函数
虚函数:virtual ReturnType func();
纯虚函数:virtual ReturnType func() = 0;
定义一个函数为虚函数,不代表这个函数不用被实现。反而,虚函数是C++里面为了实现多态性的一个工具。
定义一个函数为虚函数,是为了使用指向基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才是避免其被实现,纯虚函数的存在,是为了实现一个接口,起到一定的规范的作用,规范继承这个类的程序必须实现该纯虚函数,当父类的纯虚函数在子类中被实现,子类的该纯虚函数成员自动变为虚函数。
虚函数的使用例子见下段代码:
class A{
public:
virtual void func(){
cout << "A::func is called!" << endl;
}
};
class B:public A{
public:
void func(){
cout << "B::func is called!" << endl;
}
};
int main(){
A* a = new B();//定义一个指向A父类的指针a,并使用new实现动态联编,将其实例化为B的对象
a->func();//需要注意的是,a虽然是A的指针,但是此处通过虚函数的实现以及动态联编,其调用的是B的函数成员
return 0;
}
虚函数的“虚”体现在推迟联编和动态联编上,一个类函数的调用并不在编译的时刻被确定,而是在运行的时刻决定。
对于虚函数来说,父类和子类可以实现不同的版本,由多态方式调用的时候动态绑定
纯虚函数:
定义如上已经显示。纯虚函数一般以定义在基类中的虚函数的形式存在。纯虚函数在基类中不能被定义(前面一般的基函数允许在基类中进行定义,只是会根据程序的动态联编机制调用不同的实现),但是任何继承该基类的派生类都必须实现该纯虚函数,在实现以后,派生类继承到的纯虚函数自动转为虚函数。
纯虚函数的引入原因:
- 方便多态特性的实现,常常需要在基类中定义虚拟函数;
- 在有些程序编写情况下,并不希望对基类进行对象实例化,基类的存在只是起到一个表征作用,具体的针对具有基类特性的类的实例化,一般是针对继承了基类的派生类进行对象实例化,这些对象可以从一定程度上继承基类的一些特性。
- 使派生类成为一个单纯的继承基类某个纯虚函数的调用接口,该派生类的存在意义就是实现基类某个纯虚函数的多态性调用工具。
含有纯虚函数的基类被称为抽象类,其不被允许进行实例化;与此同时,继承了基类的派生类被要求一定得实现纯虚函数,以实现多态性。
二十四、指针和STL迭代器
指针和STL迭代器
- 迭代器不是指针,而是一种类模板,实现了对指针部分功能的形式上的封装,从而表现得像指针;
- 迭代器重载了指针使用得部分操作符,如->、*、++、–。
- 迭代器用来遍历STL容器内全部或者部分元素得对象,本质上是封装了原生指针,是对指针概念的一种提升;
- 迭代器返回对象引用,而不是对象的值,因此取值需要*等;
迭代器模式:
设计模式中存在的一种模式。简而言之,就是提供一种方法,在不暴露容器的内在表现形式的情况下,可以像指针一样依次访问容器中的元素。这种设计模式在STL中得到了广泛的应用。
准确来说,STL通过迭代器,实现了容器和算法的有机结合。只要对不同容器赋予不同的迭代器,就可以实现对不同容器的相同模式的操作,比如STL中部分容器实现的begin(), end()等。
指针:
- 指针可以指向函数等,而迭代器只能指向容器。指针从功能上看可以说是一种迭代器,但是指针只能指向特定的容器,但是迭代器是指针的抽象和繁华。
- 总而言之,指针和迭代器在使用上存在一定的相似性,尤其是对容器的遍历上,但是他们的本质天差地别。迭代器是类模板,指针是存放地址的指针变量。
二十五、const和#define的区别
角度 | const | define |
---|---|---|
常量定义 | 定的常量带类型 | 定义的常量只是个常数,不带类型 |
作用阶段 | 编译运行阶段 | 预处理阶段 |
作用方式 | 有对应的数据类型,会进行类型检查 | 简单的字符串替换,可能导致边界效应,如例1 |
空间占用 | 占用数据段空间 | 预处理以后,占用代码段空间 |
代码调试 | 可以调试 | 不能调试,因为预编译阶段已经替换完成 |
重定义 | 一旦定义就固定 | 可以通过**#undef**取消某个符号的定义 |
//例1
#define N 2+3;
double a = N/2;//实际上,a的值为2+3/2=3.5,而不是预想的2.5
可以使用#define防止头文件重复引用,比如引用的两个头文件同时定义了一个全局变量,则这两个全局变量之间会引起冲突,解决方式如下:
//例2
//将以下代码放入头文件头部,可以避免重复引用
#ifndef xxx
#define xxx
//write your code here
#endif
二十六、C++防止头文件被重复引入的三种方法
- 使用宏定义
//可能被重复引用的头文件中加入以下代码
#ifndef _NAME_H
#define _NAME_H
//头文件内容
#endif
当该头文件第一次被引用时,由于_NAME_H没有被定义过,因此进行了头文件内容的定义,但是第二次重复引用了该头文件时,由于_NAME_H已经被定义了,那么就不会重复定义。
-
使用#pragma once避免重复引入
部分较新的编译器支持#pragma one指令,将其附加到头文件开头,那么该头文件就只被引用一次。由于使用宏定义#define的方式,程序每次进行头文件引入时都要进行判断,因此其效率不高。相对于此,使用#pragma once指令具有更高的效率,缺点是不是每一种编译器都支持,因此其兼容性不是很优秀,此外,#pragma once不支持类似#define那样的对部分程序段的定义。 -
使用_Pragma操作符
C99标准新增的一个和#pragma指令类似的操作符,可以被看作是#pragma的增强版,不仅可以实现#pragma once的功能,还可以和宏搭配起来一起使用。
_Pragma("once")//置于头文件开头
除非对程序的编译效率有极高的要求,都推荐使用第一种宏定义的方式,以方便代码的可移植性。
二十七、static作用
- 在函数体内部定义的变量,该变量从程序开始到结束只会分配一次内存,当再次进入该函数的时候,其值不变,为上次退出该函数的时候的值;
- 模块内定义的static变量和函数不能被外部模块通过extern等关键字引用,除非定义一个间接变量或者函数;
- 类中定义的static成员或者函数是属于整个类的,与具体的类对象无关。像类里面的静态成员,只会在运行的时候创建一次,也就是说该类的所有对象维护该成员的同一个拷贝,并且该静态数据成员必须在类外进行定义和初始化;再比如,类中定义的静态函数,不能使用this指针,因为this指针是对象独有的,静态成员函数不含this指针;
- 静态成员函数只能引用属于该类的静态数据成员或者静态成员函数,原因是静态成员函数没有this指针;
- 类的静态成员函数不属于对象,因此可以不通过对象直接调用,直接使用类名调用。
A.cpp
static char C = 'A';
char c = C;
static int F()
{
return 5;
}
int ff()
{
return F();
}
B.cpp
int main()
{
extern int ff();
extern char c;
cout << ff() << endl;
cout << c << endl;
return 0;
}
二十八、const关键字的作用
- 定义普通变量时,其值只能创建时初始化一次,此后不可改变
- 如上,const int* ptr; int* const ptr;前者不可改变指向的值,后者不可改变指针所指地址
- 函数形参声明中,使用const拒绝实参被修改
- 在类中定义const函数,不可以修改类成员变量的值,但是可以修改传递过来的形参值
二十九、sizeof
是一个运算符,而不是函数。计算变量或者结构体大小可以不加括号,计算内建类型的大小需要加括号
三十、不能重载的运算符
- . (成员访问运算符)
- ->(成员指针运算符)
- ::(作用域解析运算符)
- ?(条件运算符)
- sizeof
三十一、C++静态多态和动态多态
静态多态:运行前确定类型或者函数调用,也就是编译以后,其采用的是函数重载和泛型编程,模板等’
动态多态:运行后确定类型或者函数调用,采用虚函数来实现
三十二、#Include后引号和尖括号区别
引号的会搜索当前工作目录下的头文件,尖括号会搜索安装目录(系统)下的头文件。
三十三、构造函数为啥不能是虚函数
虚函数存在于虚表中,虚表要靠虚表指针维护,而只有实例化以后的对象才有虚表指针,二实例化对象就必须调用构造函数,因此产生冲突。
三十四、函数声明的意义
-
以错误的方式调用函数存在一定的危险性:
函数的原型信息在编译后即不存在,包括参数的个数和类型、返回值类型等经过编译后将不被记录;
如果不要求声明函数的话,以错误的方式调用函数,会产生不可预期的结果,并且很多情况下可能不会给出错误提示; -
函数原型可以理解为主调函数与被调函数之间的协议
-
运行结果错误VS编译错误
最好使错误暴露在编译阶段。
三十五、显示类型转换
除了C风格的显示类型转化以外,C++还支持四种适合不同场景的显示类型转换方法。
- const_cast:
适合于将常量指针(引用)转换为非常量指针(引用),并且仍然指向原来的对象;
const_cast一般用于修改常量指针或引用为非常量类型,如const char* ptr的形式。
#include<iostream>
int main()
{
// 原始数组
int ary[4] = { 1,2,3,4 };
// 打印数据
for (int i = 0; i < 4; i++)
std::cout << ary[i] << "\t";
std::cout << std::endl;
// 常量化数组指针
const int*c_ptr = ary;
//c_ptr[1] = 233; //error
// 通过const_cast<Ty> 去常量
int *ptr = const_cast<int*>(c_ptr);
// 修改数据
for (int i = 0; i < 4; i++)
ptr[i] += 1; //pass
// 打印修改后的数据
for (int i = 0; i < 4; i++)
std::cout << ary[i] << "\t";
std::cout << std::endl;
return 0;
}
/* out print
1 2 3 4
2 3 4 5
*/
- static_cast
static_cast的类型转换功能和C风格的显示类型转换基本一致。static_cast没有运行时的类型检查来确保转换的安全性,因此在使用static_cast时的安全性问题需要使用者考虑。
可以应用于类层次结构中父类和子类之间指针或者引用的转换。需要注意的是,由子类指针转化为父类指针时(上行转换)是安全的,由父类指针转换为子类指针(下行转换)时是有风险的,原因是没有动态类型检查。
static_cast也可以用于基本数据类型之间的转换,比如把int转换为char等基本转换,不过其安全性也需要开发者进行维护。
与const_cast对比,static_cast不可以转换掉原类型的const等属性。根据《C++ primer》所说:任何具有明确定义的类型转换,只要不包含const,都可以使用static_cast进行转换。需要注意的是安全性问题。
/* 常规的使用方法 */
float f_pi=3.141592f
int i_pi=static_cast<int>(f_pi); /// i_pi 的值为 3
/* class 的上下行转换 */
class Base{
// something
};
class Sub:public Base{
// something
}
// 上行 Sub -> Base
//编译通过,安全
Sub sub;
Base *base_ptr = static_cast<Base*>(&sub);
// 下行 Base -> Sub
//编译通过,不安全
Base base;
Sub *sub_ptr = static_cast<Sub*>(&base);
- reinterpret_cast
如字面意思,reinterpret_Cast用于实现无关类型之间的类型转换,通常为操作数的位模式提供较低层次的重新解释。如以下代码:
#include<iostream>
#include<cstdint>
using namespace std;
int main() {
int *ptr = new int(233);
uint32_t ptr_addr = reinterpret_cast<uint32_t>(ptr);
cout << "ptr 的地址: " << hex << ptr << endl
<< "ptr_addr 的值(hex): " << hex << ptr_addr << endl;
delete ptr;
return 0;
}
/*
ptr 的地址: 0061E6D8
ptr_addr 的值(hex): 0061e6d8
*/
用于任意的指针之间的转换,引用之间的转换,指针和足够大的int型之间的转换,整数到指针的转换。
这种无关类型之间的转化有什么用呢?IBM C++对reinterpret_cast的推荐使用场景如下:
- 指针转向足够大的整数类型,用于对指针数据进行保持;
- 从整形或者枚举类型转为指针类型;
- 从一个指向对象的指针转向另一个指向不同类型对象的指针;
- 从一个指向成员的指针转向另一个指向类成员的指针,前提是转换的前后都是函数类型或者对象类型。
- dynamic_cast
dynamic_cast强制转换,是四种中最为特殊的一种,因为这涉及面向对象的多态性和程序运行时的状态,也和编译器的属性设置有关,所以就不可以被C风格的强制类型转换替代,因而也是C++中用的比较多的显示转换方法,因为与C++的多态性结合更为紧密。
见下述示例程序,由sub类指针上行转换为base类指针时,程序正常转换。反之,对于由base类指针向sub类指针的下行转换,dynamic_cast转换的结果为空指针,这意味着,dynamic_cast在进行下行转换时,有对“运行期类型信息”(Runtime type information, RTTI)进行检查。
那么这种检查来自于哪个地方呢?从程序中可见,base父类定义了未实现的虚函数,并且sub子类在继承父类时实现了一个同名并且同样为虚函数的函数签名。由于当一个类中至少有一个虚函数时,编译器就会构建一个虚函数表来指示这些虚函数的地址。当父类的子类继承并实现了同名的虚函数,那么虚函数表将会指向该子类的虚函数的地址。由此,父类和子类之间的多态性得到体现,并影响到dynamic_Cast进行强制类型转换时对RTTI的检查,从而为base->sub的转换结果赋予了一个空指针,从而实现了一定的自检和安全性。
#include<iostream>
using namespace std;
class Base{
public:
Base() {}
~Base() {}
void print() {
std::cout << "I'm Base" << endl;
}
virtual void i_am_virtual_foo() {}
};
class Sub: public Base{
public:
Sub() {}
~Sub() {}
void print() {
std::cout << "I'm Sub" << endl;
}
virtual void i_am_virtual_foo() {}
};
int main() {
cout << "Sub->Base" << endl;
Sub * sub = new Sub();
sub->print();
Base* sub2base = dynamic_cast<Base*>(sub);
if (sub2base != nullptr) {
sub2base->print();
}
cout << "<sub->base> sub2base val is: " << sub2base << endl;
cout << endl << "Base->Sub" << endl;
Base *base = new Base();
base->print();
Sub *base2sub = dynamic_cast<Sub*>(base);
if (base2sub != nullptr) {
base2sub->print();
}
cout <<"<base->sub> base2sub val is: "<< base2sub << endl;
delete sub;
delete base;
return 0;
}
/* vs2017 输出为下
Sub->Base
I'm Sub
I'm Base
<sub->base> sub2base val is: 00B9E080 // 注:这个地址是系统分配的,每次不一定一样
Base->Sub
I'm Base
<base->sub> base2sub val is: 00000000 // VS2017的C++编译器,对此类错误的转换赋值为nullptr
*/
总结
- 去const,用const_cast,如为常量指针去常;
- 一般类型转换,最贴近C风格的类型转换,用static_cast,缺点是不安全,上行安全,下行不安全;
- 最贴近C++多态性的dynamic_cast,可以基于子类、父类之间的多态性进行转换时的RTTI检查;
- reinterpret_cast用于不同类型数据之间的转换,具有特殊的用途。
三十六、静态/动态生存期
静态生存期:static定义的或者文件作用域中定义的全局变量具有静态生存期;
动态生存期:块中定义且没有用static关键字修饰的变量具有动态生存期,开始于声明之处,结束于该标识符所处作用域结束。
三十七、虚构造函数/虚析构函数
虚函数通过虚函数表(vtable)实现C++的动态绑定(多态)。
执行虚函数时,需要先查询vtable,确定函数的入口地址。然而,vtable随着对象的实例化而创建并存储,对象没有实例化,便没有vtable,因此不能实现虚构造函数。
而析构函数一般都写为虚析构函数:对于发生继承关系的两个类来说,有些时候需要通过基类指针调用子类函数。这时候要析构基类指针,如果基类析构函数不是虚函数,便不能通过动态绑定机制正确识别对象类型从而正确调用析构函数。
#include <iostream>
using namespace std;
class base
{
public:
base()
{
cout << "Base" << endl;
}
~base()
{
cout << "~Base" << endl;
}
};
class der :public base
{
public:
der()
{
cout << "der" << endl;
}
~der()
{
cout << "~der" << endl;
}
};
class der1 :public der
{
public:
der1() {
cout << "der1" << endl;
}
~der1() {
cout << "~der1" << endl;
}
};
int main() {
base *Der = new der;
cout << endl;
der *Der1 = new der1;
cout << endl;
delete Der;
cout << endl;
delete Der1;
cout << endl;
return 0;
}
在各个类析构函数前加入virtual前后运行结果: