导图社区 Cpp面经
C++面经知识点总结,包括基础,语言的区别,未分类,C++程序编译过程,面向对象编程,C++11新特性,C++内存分配的知识点。
编辑于2022-08-26 14:30:03 北京市C++面经
基础
宏定义
和函数的区别
代码长度:宏定义每次都要文本替换;一般来说代码长度会比函数长 执行速度:宏比函数快,函数调用会有额外的开销(在栈中开辟内存空间,参数传递时可能有拷贝),类型检查需要时间 操作符优先级:宏更可能出错 #define sqr(x) x*x x=5+1-->5+1*1+5=11,函数调用则为25 安全性:宏没有参数类型,无类型检查,函数会实参与形参个数是否相等及类型是否匹配或兼容 分号:宏不用加分号
代码长度
执行速度
操作符优先级
和内联函数的区别
可以用#define的时候,都能用内联函数代替 内联函数的目的是省去函数调用的开销,如果函数体比较复杂,代码段很长,函数调用的开销比不上函数内部的开销的话,就没必要用内联函数。
和typedef的区别
执行阶段:宏在预处理阶段,typedef在编译阶段 作用:宏用于定义常量和书写复杂内容,typedef用于定义类型的别名 语句:宏不是语句,不用加分号 类型检查:宏无类型,无类型检查,typedef会有类型检查
和const的区别
执行阶段:define-预处理 const-编译和运行时起作用 类型检查:define-无类型检查,不加括号易出错,const有数据类型,编译器会进行类型安全检查 内存占用: define:宏定义在程序运行过程中没被用到就不会分配内存空间,被用到一次就会有一次内存拷贝。 const在程序运行过程中只有一份
思考方向
执行阶段
实现功能
内存空间
安全性
类型检查
变量
指针和引用
指针和引用的相同点和区别?
相同
引用本质上是指针 使用引用和指针都会分配4字节的内容空间
不同
定义上看:指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名 从引用本质看:引用本质是一个常指针,引用的指向不能发生改变 -初始化上看:指针可以不初始化,引用定义时必须初始化 -从指向上看:指针可以指向空,引用不可以 -指针在初始化后可以改变指向,而引用在初始化之后不可再改变 sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小 从参数传递上看:指针传参是值传递,指针实参不会被改变;引用传递会直接改变原有的变量。
什么时候用引用,什么时候用指针?
类对象做参数传递时都用引用 基本数据类型的传递,建议使用指针,操作更方便。 如果不能保证指向为非空/指针指向频繁改变,使用指针。 如果能确定指针或引用的指向一定存在时,使用引用,因为使用指针总是要判断它是否为空 void printDouble(const double& rd) { cout << rd; // 不需要测试rd,它 } // 肯定指向一个double值 相反,指针则应该总是被测试,防止其为空: void printDouble(const double *pd) { if (pd) { // 检查是否为NULL cout << *pd; } }
区别以下指针?
数组,数组的大小为10,元素类型:int* 指针,指向一个int类型,大小为10的数组 函数声明,函数参数是int,函数返回值是int *类型 函数指针,指向的函数,其参数是int类型,返回值是int类型
int* p[10] int (*p)[10] int* p(int a) int (*p)(int a)
野指针和悬空指针
声明和定义的区别?
对于变量: 声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间。 相同变量可以在多处声明(外部变量extern),但只能在一处定义 p 对于函数: 声明:一般在头文件里,对编译器说:这里我有一个函数叫function() 让编译器知道这个函数的存在。 定义:一般在源文件里,具体就是函数的实现过程 写明函数体。
const
注意事项
必须对其进行初始化,然后无法通过赋值更改该值。
常量可以是编译时常量,也可以是运行时常量
const修饰的局部变量存放在栈区
const作用
定义常量 可以进行安全检查:const常量具有类型,编译器可以进行安全检查;#define宏定义没有数据类型,只是简单的字符串替换,不能进行安全检查 起到保护数据,防止数据修改的作用 安全检查是什么?都检查哪些内容? --保证用户代码仅在允许范围内可用 --内容:检查访问是否越界,检查代码是否符合语法(不允许修改const指定的数值)
const与指针
顶层-指针-const a 底层-对象-const*a
顶层const
指针本身是常量 任意对象是常量
char * const a
指针本身是常量
char * const a
const修饰a, a为指针名
必须初始化
指针指向的是变量
指针的指向不能改变
指针所指向对象的值可以改变
const char a
底层const
更接近对象本身
const int *a/int const *a
指针指向的对象是常量
const char* a;//指向常量的指针 char const* b;//同上 char x = '3'; a = &x;//a指向x char * tmp=&x;//tmp指向x //*a='5'//会报错,不能用指向常量的指针修改任何对象的值(包括基础对象和其他对象) *tmp = '5';//用tmp修改x cout << *a << endl;//x改变
const修饰*a,*a为指向的对象
指针的指向可改变
允许把非const对象的地址赋值给const对象的指针
不能通过指针来修改任何指向对象的值
指向常量的常指针
const char * const a
const与函数
(1)如果函数需要传入一个指针,是否需要为该指针加上const,把const加在指针不同的位置有什么区别? ---这取决于const加在指针的位置如果是: const char*a,那么不允许指针a修改任何对象的值。如果是:char* const a常指针,不允许指针指向改变 (2)如果写的函数需要传入的参数是一个复杂类型的实例,传入值参数或者引用参数有什么区别,什么时候需要为传入的引用参数加上const? 比如传入的是一个类,复制类需要有构造和析构的过程,会浪费时间,可以在引用前加入const,提高效率。但如果是简单的数据类型,复制速度很快值传递即可
const修饰返回值
没有意义
const修饰函数参数
const与类
const成员函数中不允许修改对象的成员
const对象只能访问const成员函数
非const对象可以访问任意的成员函数,包括const成员函数
static
静态变量
在函数
当变量声明为static时,空间将在程序的生命周期内分配。即使多次调用该函数,静态变量的空间也只分配一次,每一次调用中的变量值都取决于上一次调用的结果。
在类中
静态成员
静态成员在类内声明,必须在类外定义
静态函数(在类中)
就像类中的静态数据成员或静态变量一样,静态成员函数也不依赖于类的对象。我们被允许使用对象和'.'来调用静态成员函数。但建议使用类名和范围解析运算符调用静态成员(Apple::print())。 静态成员函数仅访问静态数据成员或其他静态成员函数,它们无法访问类的非静态数据成员或成员函数。 非静态成员可以访问静态和非静态成员和成员函数
作用
隐藏
当我们同时编译多个文件时,所有未加 static 前缀的全局变量和函数都具有全局可见性,会有命名冲突,加了 static会对其它源文件隐藏。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突
保持变量内容的持久
存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化,变量存在到程序生命周期结束。共有两种变量存储在静态存储区:全局变量和 static 变量
默认初始化为0
作用于类
初始化时机
基本数据类型
编译时
类
首次用到时,初始化
结构体
字节对齐
定义
一个变量的地址能够被该变量的所占的字节数整除
原因
CPU 只能访问对齐地址上的固定长度的数据 假设一个32位的cpu,它读取数据只能从0x00-0x03,如果一个32位的数据是0x01-0x04,没有进行字节对齐,就需要访问0x00-0x03和0x04-0x07的地址,访问两次比访问一次效率低
结构体对齐准则
对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍 结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍 如程序中有#pragma pack(n)预编译指令,则所有成员对齐的基准值为n和成员大小的最小值 结构体内的函数不占用内存空间
如何获取变量相对于结构体的偏移量
使用<stddef.h>头文件中的,offsetof宏。 举个例子:在Visual Studio 2019 + Win10 下的输出情况如下 当然了,如果加上 #pragma pack(4) 指定4字节对齐方式就可以了。S结构体中各个数据成员的内存空间划分如下所示,需要注意内存对齐 #include <iostream> #include <stddef.h> using namespace std; struct S { int x; char y; int z; double a; }; int main() { cout << offsetof(S, x) << endl; // 0 cout << offsetof(S, y) << endl; // 4 cout << offsetof(S, z) << endl; // 8 cout << offsetof(S, a) << endl; // 12 return 0; }
struct和class的区别
相同点
都有成员函数,公有,私有部分 能用class完成的也可以用struct完成
不同
struct默认public继承,class默认private继承 对于这两者的成员,如果不指定公私有struct默认public,class默认private
类对象大小的存储空间
非静态成员的数据类型大小之和。 编译器加入的额外成员变量(如指向虚函数表的指针)。 为了边缘对齐优化加入的填充空间 空类对象大小为1字节
union
默认访问控制符为 public 可以含有构造函数、析构函数 不能含有引用类型的成员 不能继承自其他类,不能作为基类 不能含有虚函数 匿名 union 在定义所在作用域可直接访问 union 成员 匿名 union 不能包含 protected 成员或 private 成员 全局匿名联合必须是静态(static)的
多个变量共享内存空间
union test { char mark; long num; float score; };
语言区别
C++VS C
C++VS JAVA
面向过程:把要实现的功能拆分为一个一个执行步骤,按分析好的流程流程的一步步执行
未分类
类型检查
检查值的使用是否符合规范,如string a='1';string类型赋值不对 在函数中,会检查函数调用时形参和实参个数是否匹配,传入的参数是否符合类型等
strlen和sizeof的区别
sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。 sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是'\0'的字符串。 因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。
拷贝初始化和直接初始化的区别?
string str1 = "first"; //拷贝初始化,编译器允许把这句话改写为string str(“first”),但是string类必须有public的拷贝(移动)构造函数 string str2(10,'a'); //直接初始化 string str3(str2); //直接初始化 string str4 = string(10,'b'); //拷贝初始化 string str5 = str4; //拷贝初始化 string str6 ("strr"); //直接初始化
直接
只是直接调用类的构造函数或拷贝构造函数
拷贝
先创建一个对象(执行构造函数)再进行拷贝(执行拷贝构造函数)
初始化和赋值的区别?
简单的数据类型区别不大 复杂的数据类型或者类,就有区别: 初始化:会调用构造函数(因为初始化的定义为创建变量时赋予它一个值) 赋值:用到重载运算符=
形参和实参有什么区别?
1) 形参变量只有在被调用时才分配内存单元,在调用结束时, 即刻释放所分配的内存单元。因此,形参只作用于在函数内部。 2) 函数调用时,实参都必须具有确定的值, 以便把这些值传送给形参。 3) 实参和形参在数量上,类型上,顺序上应严格一致, 否则会发生“类型不匹配”的错误。 4) 形参的值发生改变,而实参中的值不会变化。函数调用中发生的数据传送是单向的。 即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。
构造函数和析构函数是否要被设为inline函数?
无意义,编译器不会采取内联的建议,因为构造和析构函数没有看上去那么简单,因为它会有申请释放内存,构造/析构对象的操作
main函数前后都干了什么?
main函数执行前:主要是初始化系统相关的资源 初始化静态变量和全局变量 将未初始化部分的全局变量赋初值:数值赋值为0,bool为FALSE,指针为NULL等。 全局对象初始化,在main之前调用构造函数,这是可能会执行前的一些代码 将main函数的参数argc,argv等传递给main函数,然后才真正运行main函数 __attribute__((constructor)) main函数执行之后: 全局对象的析构函数会在main函数之后执行; 可以用 atexit 注册一个函数,使该函数在main 之后执行; __attribute__((destructor))
atexit
C 库函数 int atexit(void (*func)(void)) 当程序正常终止时,调用指定的函数 func
attribute
被__attribute__((constructor/destructor))修饰的函数,该函数会在Main函数执行前/后被调用
GCC,gcc,g++,GNU有什么区别?
GNU:自由操作系统 GCC:GNU Compiler Collection(GUN 编译器集合),它可以编译C、C++、JAV、Fortran、Pascal、Object-C、Ada等语言。 gcc是GCC中的GUN C Compiler(C 编译器) g++是GCC中的GUN C++ Compiler(C++编译器)
函数传参时什么时候用引用?
对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小 传递类的对象或者其他复杂数据结构时用引用 如果局部变量作为函数返回值时,函数返回值需要用指针类型
C++异常处理
try throw catch
try包裹语句块 在try块中通过throw抛出某一种类型的异常 然后程序马上跳转到与异常类型匹配的catch块,进行异常处理。在throw之后catch之前的代码都不会执行
子主题
include <iostream> using namespace std; int main() { double m = 1, n = 0; try { cout << "before dividing." << endl; if (n == 0) throw - 1; //抛出int型异常 else if (m == 0) throw - 1.0; //拋出 double 型异常 else cout << m / n << endl; cout << "after dividing." << endl; } catch (double d) { cout << "catch (double)" << d << endl; } catch (...) { cout << "catch (...)" << endl; } cout << "finished" << endl; return 0; } //运行结果 //before dividing. //catch (...) //finished
异常出现的情况
数组越界访问(out_of_range) 除法计算除数为0 动态分配内存失败(bad_alloc)
C++标准的异常
exception类
bad_alloc
logic_error
out_of_range
length_error
runtime_error
overflow_error
range_error
underflow_error
ifndef的作用?
防止头文件被重复包含和重复编译 头文件重复包含会增大程序大小,重复编译增加编译时间,降低运行效率
深拷贝和浅拷贝的区别?
深拷贝:开辟新的内存空间存储拷贝的值,互不影响 浅拷贝:只拷贝了一个指针,拷贝的指针和原指针指向同一块内存地址,如果两个对象指向的资源相同,当析构时会释放共享内存两次,就出错了。
例子
#include <iostream> #include <string.h> using namespace std; class Student { private: int num; char *name; public: Student(){ name = new char(20); cout << "Student" << endl; }; ~Student(){ cout << "~Student " << &name << endl; delete name; name = NULL; }; Student(const Student &s){//拷贝构造函数 //浅拷贝,当对象的name和传入对象的name指向相同的地址 name = s.name; //深拷贝 //name = new char(20); //memcpy(name, s.name, strlen(s.name)); cout << "copy Student" << endl; }; }; int main() { {// 花括号让s1和s2变成局部对象,方便测试 Student s1; Student s2(s1);// 复制对象 } system("pause"); return 0; } //浅拷贝执行结果: //Student //copy Student //~Student 0x7fffed0c3ec0 //~Student 0x7fffed0c3ed0 //*** Error in `/tmp/815453382/a.out': double free or corruption (fasttop): 0x0000000001c82c20 *** //深拷贝执行结果: //Student //copy Student //~Student 0x7fffebca9fb0 //~Student 0x7fffebca9fc0
左值和右值
左值
表示的是可以获取地址的表达式,它能出现在赋值语句的左边,对该表达式进行赋值。但是修饰符const的出现使得可以声明如下的标识符,它可以取得地址,但是没办法对其进行赋值 const int& a = 10;
左值引用
就是引用
右值
表示无法获取地址的对象,有常量值、函数返回值、lambda表达式等。无法获取地址,但不表示其不可改变,当定义了右值的右值引用时就可以更改右值。
右值引用
可获取右值或左值的地址
string和char*区别
string继承自basic_string,其实是对char*进行了封装,封装的string包含了char*数组,容量,长度等等属性。 string可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2^n),然后将原字符串拷贝过去,并加上新增的内容。
strcpy和memcpy的区别
1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。 2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢 出。memcpy则是根据其第3个参数决定复制的长度。 3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
i++和++i的区别
++i:先自增,再赋值 i++:先赋值,再自增 效率区别: 对于简单数据类型差不多 对于自定义数据类型,比如类: i++是先用临时对象保存原来的对象,然后对原对象自增,再返回临时对象,需要调用两次拷贝构造函数与析构函数(将原对象赋给临时对象一次,临时对象以值传递方式返回一次) ++i返回的是一个引用,相当于对原来的对象进行操作,不会产生临时对象。
大小端存储
大端:内存的高地址对应待存储数据的低字节(网络序) 小端:内存的高地址对应待存储数据的高字节(主机序) 0X12345678(越靠近0X代表字节越高) p
大网-小主
代码实现判断
p
extern"C" 用法
作用
在C++中调用C语言代码 p
(1)C++代码中调用C语言代码; (2)在C++中的头文件中使用; (3)在多个人协同开发时,可能有人擅长C语言,而有人擅长C++;
C++ 程序编译过程
预处理:#include头文件以及宏定义替换成其真正的内容,预处理完成后得到的仍为文本文件.i,文件大小和代码数量都会增加(头文件存放变量的定义) 编译:将预处理之后的程序转换成特定汇编代码.s (mov ax ,bx) 汇编 :将汇编代码转化为二进制格式的机器码(生成目标文件.o) 链接:将目标文件与库文件链接生成可执行文件(库文件的内容就是头文件的具体实现)
静态/动态类型,静态/动态绑定
静态类型:对象在声明时采用的类型,在编译期既已确定; 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的; 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期; 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期; 非虚函数一般都是静态绑定,而虚函数都是动态绑定
.h和.cpp的区别?
引用如何动态绑定?
通过虚函数 #include <iostream> using namespace std; class Base { public: virtual void fun() { cout << "base :: fun()" << endl; } }; class Son : public Base { public: virtual void fun() { cout << "son :: fun()" << endl; } void func() { cout << "son :: not virtual function" <<endl; } }; int main() { Son s; Base& b = s; // 基类类型引用绑定已经存在的Son对象,引用必须初始化 s.fun(); //son::fun() b.fun(); //son :: fun() return 0; }
你知道Debug和release的区别是什么吗?
debug:包含调试信息,所以容量比Release大很多,并且不进行任何优化 release:不对源代码进行调试,编译时对应用程序的速度进行优化,使得程序在代码大小和运行速度上都是最优的
面向对象编程
对象?
对现实中具体事物的模拟,具有属性和行为
什么是类?
同一类对象的共同属性和行为
什么是面向对象?
面向对象是指把具体的事物抽象成类,类中包含数据(成员变量)和动作(成员函数)
面向对象的优势?
面向对象编程,模块之间解耦(减少依赖程度), 调用更简单, 易于修改和维护, 适合大型项目
面向对象的三大特性?
封装
这样可以降低了程序的复杂性,用户操作更方便。 保护内部数据不被滥用,就比如vector里的at函数,访问元素时会先判断下标是否合法,如果下标越界,会抛出异常; 封装有利于类的修改(我们可以更改类的实现方式,用户使用类的程序不需要修改)
提供公共接口给用户访问,而隐藏实现的细节。
继承
实现继承:将父类完全继承,不需要在额外编写代码。 接口继承:只继承接口,具体实现需要子类完成。
使一个类实现现有类的所有功能,在不需要重新编写原来类的情况下,对这些功能进行扩展
多态
定义
在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数
实现机制
虚函数表和虚表指针
(1)编译器在发现基类中有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址 (2)编译器会在每个对象的前四个字节中保存一个虚表指针,即vptr,指向对象所属类的虚表。在构造时,根据对象的类型去初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数 (3)所谓的合适时机,在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表 (4)当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面这样指向派生类的基类指针在运行时,就可以根据派生类对虚函数重写情况动态的进行调用,从而实现多态性
为什么要这样实现?
覆盖,隐藏,重载都是什么?有何区别?
重载
是指同一可访问区(同一个类中)内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
作用于同一个类中 函数名相同 参数列表不同/参数列表相同但有一个加了const 不关心返回值 不关心是否为虚函数 不关心形参名字
p
在编译时就确定调用哪个函数
隐藏
若子类函数与父类的同名函数,无论返回值和参数是否相同,父类函数都会被子类屏蔽
作用于父类和子类之间 函数名相同
覆盖
父类中有一个虚函数,子类中存在与该虚函数,函数名、参数列表、返回值类型都相同的函数,则当子类调用时,会调用子类覆盖的函数。
作用于父类和子类之间 函数名-参数列表-返回值相同 父类指针指向子类 父类必须为虚函数 不关心子类是否为虚函数
运行时确定调用哪个函数
成员函数里memset(this,0,sizeof(*this))会发生什么
破坏虚函数指针 如果类中复合有其他的对象,若此时该对象已初始化,则会破坏该对象的内存空间
类
类是创建对象的模板, 类是对象的抽象
类的关系
组合
一个类A包含另一个类B
构造和析构函数的先后顺序?s
内部先构造 外部先析构
复合和继承的优缺点?
组合 优点: 不会破环封装性父类的任何变化不会引起子类的变化 整体类可以对局部类的接口进行封装,提供新的接口 缺点: 整体类不能自动获得和局部类同样的接口,只有通过创建局部的对象去调用它; 创建整体类的时候需要创建局部类的对象 2.继承 优点: 子类继承了父类能自动获得父类的接口 创建子类对象的时候不用创建父类对象 缺点: 破坏了封装,父类的改变必定引起子类的改变,子类缺乏独立性 支持功能上的扩展,但多重继承往往增加了系统结构的复杂度。
委托
A类内部的指针指向B类
by reference
A为接口,B为具体实现
访问说明符
public,protected,private的区别
public
成员函数一般设为public 可被任意实体访问 public-继承:完全继承基类的public,private,protected
struct的默认继承方式
private
变量一般设为private 只有类内的成员和友元函数可以访问,不允许类的对象访问 private-继承:基类所有的成员都变成private
class的默认继承方式
protected
可以被本类和子类,友元函数访问,不允许类的对象访问 protected继承:与基类的pirvate和protected相同,基类的public变成了protected
友元
友元函数/友元类
访问另一个类中的私有或保护成员 的机制
优缺点
优点
提高了程序的运行效率
缺点
破坏了类的封装性和数据的透明性
注意
没有传递性
C是B的友元,B是A的友元,C不是A的友元
没有继承性
A 是B的友元,C继承B,A不是C的友元
虚函数
什么是虚函数?
被 virtual 关键字修饰的成员函数,就是虚函数
纯虚函数
是什么?
在基类中声明的虚函数,在函数原型后加“=0”就变成了纯虚函数。 纯虚函数没有定义,它需要在子类中实现。
作用?
构造抽象类 希望让子类自己实现某些具体功能
为什么要用它?
基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理
虚函数与纯虚函数的选择?
当需要子类实现新功能,但父类也有缺省的备选方案时使用虚函数(电脑->关机) 若要求功能必须由子类实现,父类没有方案时,使用纯虚函数(动物->进食种类,木材->改造方式)
虚函数实现机制
在编译过程中,会生成虚函数表,虚函数表存储着的类的虚函数地址,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,所调用函数的地址从的是子类关联的虚函数表中选取
动态绑定的三个条件?
虚函数与纯虚函数的区别?
定义
虚函数需要有实现,纯虚函数没有具体实现,纯虚函数需要在虚函数声明的基础上加上关键字=0; 虚函数可以直接使用,纯虚函数不能。 拥有纯虚函数的类不能实例化,仅拥有虚函数的类可以实例化
虚函数表放在什么区?
虚函数表放在常量区。理由:类似静态成员变量,类的对象共享 虚函数放在代码区
抽象类
是什么?
拥有纯虚函数的类
特点?
抽象类不能生成对象 它要求子类在重写了抽象类的纯虚函数后,才能创建实例 如果子类中还有纯虚函数,那子类也还是抽象类,不能生成对象
作用?
定义了一种接口,它要求某些功能必须由子类来实现,更符合现实生活中的情况 哪些情况?比如动物作为父类,它的子类有牛,狮子等等,假设这些类都拥有一个成员函数显示该动物进食的种类,但是动物这个父类没有默认的进食种类,需要子类来实现,比如牛是食草,狮子是食肉,所以动物需要定义为抽象类
构造函数
构造函数、析构函数是否需要定义成虚函数?为什么?
构造函数
虚函数对应一个vtable(虚函数表),类中存储一个vptr指向这个vtable。如果构造函数是虚函数,就需要通过vtable调用,可是对象没有初始化就没有vptr,无法找到vtable,所以构造函数不能是虚函数。
析构函数
由于类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。 如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。 所以将析构函数声明为虚函数是十分必要的。在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,要将基类的析构函数声明为虚函数
构造函数、析构函数是否需要定义成内联函数?为什么?
将构造函数和析构函数声明为inline是没有什么意义的,即编译器并不 真正对声明为inline的构造和析构函数进行内联操作,因为编译器会在构造和析构函数中添加额外的操作(申请/释放内存,构造/析构对象等),致使构造函数/析构函数并不像看上去的那么精简。
析构函数何时调用?
1) 对象生命周期结束,被销毁时; 2) delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时; 3) 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。
种类?
默认
Student(){//默认构造函数,没有参数 this->age = 20; this->num = 1000; };
定义
在调用时不需要显示地传入实参的构造函数
编译器什么时候会合成默认构造函数?
默认构造函数”被需要“的时候编译器才会合成默认构造函数
什么时候会被需要?
该类含有类对象数据成员,且该类对象类型有默认构造函数 一个类继承于父类,且父类有默认构造函数 该类有虚函数时: 原因是含有虚函数的类对象都含有一个虚表指针vptr,编译器需要对vptr设置初值以满足虚函数机制的正确运行,vptr设置初值以满足虚函数机制的正确运行,编译器会把这个设置初值的操作放在默认构造函数中 虚基类
如果自己定义了构造函数,编译器不会生成合成的默认构造函数
初始化
Student(int a, int n):age(a), num(n){}; //初始化构造函数,有参数和参数列表
什么时候必须用到?
存在必须初始化的变量
初始化引用 初始化常量
复合和继承
调用基类构造函数&&有基类构造函数有参数 调用类成员构造函数&&基类构造函数有参数
拷贝
Student(const Student& s){//拷贝构造函数,这里与编译器生成的一致 this->age = s.age; this->num = s.num; };
调用时机
用类的一个实例化对象去初始化另一个对象的时候 函数的参数是类的对象时(非引用传递)
转换
//转换构造函数,形参是其他类型变量,且只有一个形参 Student(int r){ this->age = r; this->num = 1002; };
其他知识
阻止对象呗实例化的方法
1) 将类定义为抽象基类或者将构造函数声明为private; 2) 不允许类外部创建类对象,只能在类内部创建对象
成员对象初始化放在构造函数之前,编译器会首先调用default构造函数,分配内存并初始化,赋值构造函数只是进行赋值而已,并非初始化
p
使用初始化列表能够在定义时直接初始化,省略了赋值步骤
p
泛型编程
转换函数
无返回类型
通常加const
explicit
class Point { public: int x, y; explicit Point(int x = 0, int y = 0) : x(x), y(y) {} }; void displayPoint(const Point& p) { cout << "(" << p.x << "," << p.y << ")" << endl; } int main() { displayPoint(Point(1)); Point p(1); }
明白的,显式的
只修饰构造函数,声明:不能让对象显式转换
C++11新特性
auto,decltype关键字 auto:让编译器在编译时就推导出变量的类型;auto需要马上初始化,不能定义数组,可以定义指针,不能用作函数参数 decltype:用于在编译器编译时推导表达式类型,不用初始化 lambda表达式:定义了一个匿名函数 (1)可以在函数内距离较近的地方定义,方便使用。 (2)可以简化起一个简洁的名字。 (3)对于很多一次性的函数,定义lambda函数可以简化代码,而不用修改源代码。
auto
让编译器在编译时根据初始值推导出变量的类型;auto需要马上初始化,不能定义数组,可以定义指针,不能用作函数参数 //普通;类型 int a = 1, b = 3; auto c = a + b;// c为int型 //const类型 const int i = 5; auto j = i; // 变量i是顶层const, 会被忽略, 所以j的类型是int auto k = &i; // 变量i是一个常量, 对常量取地址是一种底层const, 所以b的类型是const int* const auto l = i; //如果希望推断出的类型是顶层const的, 那么就需要在auto前面加上cosnt //引用和指针类型 int x = 2; int& y = x; auto z = y; //z是int型不是int& 型 auto& p1 = y; //p1是int&型 auto p2 = &x; //p2是指针类型int*
decltype
用于在编译器编译时推导表达式类型,可以不用初始化 只想要数据类型,不想要值 int func() {return 0}; //普通类型 decltype(func()) sum = 5; // sum的类型是函数func()的返回值的类型int, 但是这时不会实际调用函数func() int a = 0; decltype(a) b = 4; // a的类型是int, 所以b的类型也是int
null和nullptr
C语言中:NULL为为(void*)0 而在C++语言中,NULL则被定义为整数0,不像C一样 p C++中nullptr是指针类型,用于解决运算符重载,用来区分参数的不同输入类型,但是无法区分指针和指针 #include <iostream> using namespace std; void fun(char* p) { cout<< "char* p" <<endl; } void fun(int* p) { cout<< "int* p" <<endl; } void fun(int p) { cout<< "int p" <<endl; } int main() { fun((char*)nullptr);//语句1 fun(nullptr);//语句2 fun(NULL);//语句3 return 0; } //运行结果: //语句1:char* p //语句2:报错,有多个匹配 //3:int p
智能指针
智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的。C++11 中封装在了 <memory> 头文件中。 C++11 中智能指针包括以下三种: 共享指针(shared_ptr):资源可以被多个指针共享,使用引用计数表明资源被几个指针共享。当指针被拷贝或赋值时,引用计数+1,当指针的指向改变或被释放时,引用计数-1,当计数减为 0 时,会自动销毁指向的对象并释放相关联的内存空间,从而避免了内存泄漏。 独占指针(unique_ptr):资源只能被一个指针占有,且指针不能拷贝构造和赋值。但它的所有权可以在unique_ptr指针中转交 弱指针(weak_ptr):指向 share_ptr 指向的对象,它不会改变share_ptr的引用计数,能够解决由shared_ptr带来的循环引用问题。 循环引用:两个对象互相使用一个shared_ptr成员变量指向对方的会造成循环引用。导致引用计数失效 p p
与面向对象相关
override
override:指定子类的虚函数是重写的,也就是说override指定的虚函数必须在父类出现过,这个函数的函数名不小心打错了,编译器不让通过,如果没有override,就不会报错,但是重写的目的没有达到。 p
final
final:当不希望某个类被继承,或不希望某个虚函数被重写时,可以在类名和虚函数后添加final关键字,如果被final修饰后的类或虚函数出现继承或重写,编译器会报错。 p
基于范围的for循环
设计模式
软件设计中常见问题的典型解决方案
单例模式
定义
一个类只有一个实例, 每次使用都只能使用一个实例
为什么用?
某些对象频繁创建和销毁会占用大量资源,且该对象需要经常复用的情况就该使用单例模式。
实现方式
在类中添加一个私有静态成员变量用于保存单例实例。 声明一个公有静态构建方法用于让客户端获取单例实例。 在静态方法中实现"延迟初始化"。该方法会在首次被调用时创建一个新对象,并将其存储在静态成员变量中。此后该方法每次被调用时都返回该实例。 将类的构造函数设为私有。类的静态方法仍能调用构造函数,但是其他对象不能调用。
使用场景
网站在线人数统计 数据库连接池:一般连接操作都是固定的,如果每次都创建新对象开销会很大。复用一个连接池可以解决这个问题
优缺点
优点:节约系统资源,只有一个实例 缺点:不够抽象,扩展比较麻烦,必须直接修改代码;如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
分类
恶汉式
创建对象时初始化
懒汉式
第一次被调用时初始化
双检锁
保证线程安全
假设获取实例化对象的函数为geiinstance,在进入getinstance后,在创建对象的条件为:可获得锁且对象为空,当对象创建成功后,即使其他线程也能获取锁,但是此时对象不为空,所以就不会进入对象创建的过程,而是直接使用已有对象
什么是线程安全?
确保在多条线程访问某一资源的时候,我们的程序还能按照我们预期的行为去执行
主题
C++内存分配
简述
栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。 堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。 全局区/静态存储区(.bss 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。 常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束自动释放。 代码区(.text 段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。
堆栈的区别
申请方式不同。 栈由系统自动分配和释放。 堆是程序员申请和释放的。 申请大小限制不同。 栈顶和栈底是之前预设好的,栈是向低地址扩展,大小固定 堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。 申请效率不同。 栈由系统分配,速度快,不会有碎片。 堆由程序员分配,速度慢,且会有碎片。 栈空间默认是4M, 堆区一般是 1G - 4G 速度 操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,有专门的指令执行,所以栈的效率比较高也比较快 堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢
方式
大小
效率
速度
栈
C++压栈过程
当main函数开始调用func()函数时,编译器此时会将main函数的运行状态进行压栈,再将func()函数的返回地址、func()函数的参数从右到左、func()定义变量依次压栈; void func(int param1, int param2) { int var1 = param1; int var2 = param2; printf("var1=%d,var2=%d", f(var1), f(var2));//如果将printf换为cout进行输出,输出结果 则刚好相反 } int main(int argc, char* argv[]) { func(1, 2); return 0; }
临时变量作为返回值
临时变量,在函数调用过程中是被压到程序进程的栈中的,当函数退出时,临 时变量出栈,即临时变量已经被销毁,临时变量占用的内存空间没有被清空,但是可以被分配给其他变量,所以有可能在函数退出时,该内存已经被修改了,对于临时变量来说已经是没有意义的值了
数据区
常量
数字常量
1,2,4444,65536
字符串常量
'c',"hello world"
变量
内存泄露
什么是内存泄露?
由于疏忽或错误导致的程序未能释放已经不再使用的内存
什么操作会导致内存泄露?
创建堆但未释放时 指针重新赋值时
如何防止内存泄露?
内部封装:将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。 new和delete对应,new[]和delete[]对应 析构函数设为虚函数 智能指针
动态内存分配
new delete ,malloc free
名词解释
CRT:C runtime library,C语言运行时库函数 C++ Standard Library ,C++标准库 STL:Standard Template Library,C++标准模板库,属于C++标准库
区别
有几种new方式?
三种: plain new 就是一般的new,分配失败时会抛出错误bad_alloc nothrow new,分配失败时指针为空,不抛出错误 placement new,在一块已分配的内存空间上重新构造对象,只要已分配的内存空间足够大,就不会出现分配失败的情况。需要注意的时,使用placement new 的时候,不要使用delete释放该指针,因为这样做原来的内存空间不一定会被完全释放掉,可能会造成内存泄露,应要显示的调用对象的析构函数。
#include <iostream> #include <string> using namespace std; class ADT{ int i; int j; public: ADT(){ i = 10; j = 100; cout << "ADT construct i=" << i << "j="<<j <<endl; } ~ADT(){ cout << "ADT destruct" << endl; } }; int main() { char *p = new(nothrow) char[sizeof ADT + 1]; if (p == NULL) { cout << "alloc failed" << endl; } ADT *q = new(p) ADT; //placement new:不必担心失败,只要p所指对象的的空间足够ADT创建即 可 //delete q;//错误!不能在此处调用delete q; q->ADT::~ADT();//显示调用析构函数 delete[] p; return 0; } //输出结果: //ADT construct i=10j=100 //ADT destruct
new->operator new ->malloc
delete->operator delete->free
new 和 delete如何实现?
new
对于简单数据类型:只掉用 operator new分配内存。 对于复杂数据类型: 首先调用名为operator new的标准库函数,分配足够大的原始为类型化的内存,以保存指定类型的一个对象 接下来运行该类型的一个构造函数,用指定初始化构造对象 最后返回指向新分配并构造后的的对象的指针
new[]
先调用operator new[]分配内存 然后在指针的前四个字节写入数组大小n (对于复杂数据类型)然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小
delete
简单数据类型:free 复杂数据类型: 对指针指向的对象运行适当的析构函数 然后通过调用名为operator delete的标准库函数释放该对象所用内存
delete[]
delete[]与new[]对应,由于new[]时已记录数组的大小n,所以delete[]时会取出n,并调用n次析构函数,和n次operate delete
new[]搭配delete会怎么样?
内存泄露,泄露的不是一大块内存,而是因为delete只调用一次析构函数,只有数组首地址的内存空间被释放,其他的内存空间没有被释放,导致内存泄露
为什么有malloc和free还要用new delete?
malloc/free和new/delete都是用来申请内存和回收内存的。 在对非基本数据类型的对象使用的时候,对象创建的时候还需要执行构造函数,销毁的时候要执行析构函数。而malloc/free是库函数,是已经编译的代码,所以不能把构造函数和析构函数的功能强加给malloc/free,所以new/delete是必不可少的。
malloc申请能用delete释放吗?
malloc /free的操作对象都是必须明确大小的,而且不能用在动态类上。 new 和delete会自动进行类型检查和大小,malloc/free不能执行构造函数与析构函数,所以动态对象它是不行的。
linux下 malloc底层实现?
内存块存储方式:链表 内存块头部大小假设为 blocksize,头部数据有: size:头部大小 prev:指向前一个内存块的指针 flag:标志是否可用 存在两个指针 1.heap_end:堆内存中最后一个字节的位置 2. list_end:链表中指向最后一个已分配内存块的指针 如果申请内存大于128K,执行系统调用mmap(),释放时执行系统调用mummap() 如果小于128k,就调用brk()来分配内存。 从链表头开始,寻找首个满足大小要求的内存块,将flag设为true。 若没找到,则创建新的内存块,初始化头部数据。 无论找没找到,最后都要更新heap_end,list_end指向的位置。 这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系;
类型安全
定义
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域。如果某个编程语言或程序是“类型安全”的,那么它们就会提供类型保障的机制
举例
操作符new返回的指针类型严格与对象匹配,而不是void* 引入const关键字代替宏定义常量 内联函数代替宏定义
STL
哈希表解决冲突的方法?
线性探测 使用hash函数计算出的位置如果已经有元素占用了,则向后依次寻找,找到表尾则回到表头,直到找到一个空位 开链 每个表格维护一个list,如果hash函数计算出的格子相同,则按顺序存在这个list中 再散列 发生冲突时使用另一种hash函数再计算一个地址,直到不冲突 共享内存
解释一下什么是trivial destructor
一般是指用户没有自定义析构函数,而由系统生成的 用户自定义了析构函数,则称之为“non-trivial destructor”,这种析构函数如果申请了新的空间一定要显式的释放,否则会造成内存泄露
vector/静态数组分配在堆还是栈
指针在栈
数据内容在堆
EFFECTIVE C++
1. 让自己习惯C++
1. 视C++为语言联邦
1. c语言
2. 面向对象
3. 模板
4. STL
2. 尽量用const,enum,inline代替#define
1. 常量:const/enum代替
2. 函数:inline代替
3. 尽可能用const
4. 确定对象初始化
1. 区分开初始化和赋值
1. default函数先初始化了,然后又赋值了一次 p
1. 赋值
2. 初始化
2. p
2. 构造,默认,赋值
1. 了解C++默默编写并调用了哪些函数
1. copy构造函数 copy assignment操作符 析构函数 default构造函数
2. 不使用自动生成的函数,就应该拒绝它
2. 可将成员函数设为private并不给予实现
3. 多态基类声明virtual析构函数
3. 带多态性质的基类应该声明virtual虚构函数,任何带有virtual函数的class的析构函数都应该为virtual 如果一个类不作为基类或不想让它有多态性,就不该声明为virtual析构函数
4. 别让异常逃离析构函数
5. 不要在构造和析构函数中调用virtual函数
5. 因为这样的调用不会被子类继承,只会在基类执行
6. 令operator=返回this指针
7. 在operator=中处理自我赋值
8. 复制对象时不要忘记复制每一个成员
3. 资源管理
1. 以对象管理资源
2. 及时删除动态分配的对象,实例:shared_ptr,auto_ptr
3. 在资源管理类中小心copying行为
4. new和delete采用相同形式
4. new中有[],delete中必须有[] new中无[],delete不能有[] p
5. 提供对原始资源的访问
6. 以独立语句讲newed对象植入智能指针
6. p
4. 设计与声明
1. 让接口容易被正确使用,不易被误用
2. 设计class犹如设计type
3. 用引用传递代替值传递
3.1. 对于用户自定义类型或复杂类型用引用传递
3.2. 对于简单数据类型,stl迭代器和函数对象,用值传递
侯捷C++
C++的生前死后
C++程序的进入点:Startup Code
在main函数调用之前,必须有一个启动函数,main函数必须在启动函数中调用,否则main函数不会执行。 启动代码有多个版本,如调用WinMain,main等
不是main
具体调用层级
p
主要代码
p
main前具体做了什么?
内存分配
heap_init()
堆内存初始化: 为SBS(小堆内存)初始化:SBH:small block heap :小堆内存。 使用场景实例:比如在I/O初始化中需要分配内存,在这个过程中会先判断所需内存是否到达阈值(threshold),如果比阈值小就从小堆内存中分配,否则就交给操作系统来分配。
ioinit()
初始化输入输出(一个程序的I/O指代了程序与外界的交互,包括文件、管道、网络、命令行、信号等。) 将所有小堆内存的内存空间通过链表串联起来,方便管理。
内存管理
概述
内存管理层次
p
new delete
new底层调用了malloc
delete底层调用了free
面向对象程序设计