整理自《黑马程序员C++》课程笔记总结_黑马程序员c++语言讲义 网盘-CSDN博客
interview/README.md at master · huihut/interview (github.com)
基础知识
变量
作用:给一段指定的内存空间起名,方便操作这段内存。
常量
#define 常量名 常量值
const 数据类型 常量名 = 常量值
标识符命名规则
- 标识符不能是关键字
- 标识符只能由字母、数字、下划线组成
- 第一个字符必须为字母或者下划线
- 标识符中字母区分大小写
数据类型
C++规定在创建一个变量或者常量的时候,必须要指定出相应的数据类型,否则无法给该变量分配内存空间。
sizeof关键字 统计数据类型所占空间的大小。
实型(浮点型)
编译器会默认把一个小数当做双精度
默认情况下输出一个小数会显示出6位有效数字
字符型
显示字符型变量时用单引号括起来,不是双引号。
- C和C++中字符型变量只占1个字节。
- 字符型变量并不是把字符本身放到内存中存储,而是将对应的ASCII编码放入到存储单元中。
- 转义字符 \n \r \t \' \" \\
字符串
C风格字符串
char 变量名[] = "字符串值";
C++风格字符串
string 变量名 = "字符串值";
布尔类型bool 占1个字节大小
+ - * / % ++ --
两个整数相除结果还是整数
两个小数相除结果还是小数
(以左边为准
两个数相除除数不可以为0
选择结构
if语句
if(条件){//条件满足执行的语句}
三目运算符
表达式1?表达式2:表达式3
switch语句
switch(表达式)
{
case 结果1:
执行语句;
break;
......
default:
执行语句;
break;
}
循环结构
while循环语句
while(循环条件){ 循环语句}
do{
循环语句
}while(循环条件)
for循环语句
for(起始条件;条件表达式;末尾循环体){ 循环语句}
goto语句
goto sb;......
sb:......
数组
特点1:数组中的每个数据元素都是相同的数据类型。
特点2:数组是由连续的内存位置组成的。
数据类型 数组名[数组长度];
数据类型 数组名[数组长度] = {值1,值2......};
数据类型 数组名[] = {值1,值2......};;
二维数组
数据类型 数组名[行][列];
数据类型 数组名[行][列] = {{数据1,数据2},{数据3,数据4}};
数据类型 数组名[行][列] = {数据1,数据2,数据3,数据4};
数据类型 数组名[][列] = {数据1,数据2,数据3,数据4};
(行可以省略,列不可以
函数
返回值类型,函数名 ,参数,函数体语句,return表达式
函数的声明
作用:告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。
函数的声明可以有很多次,定义只能有一次。
在.h的头文件里面放函数声明,函数的定义放到.c文件里
指针
间接访问内存。内存一般用16进制数字标识。
在32位占4个字节。在64位占8个字节。
空指针 指向内存中编号为0的空间 不可以访问的
int* p = NULL;
野指针
指针变量指向非法的内存空间。
const修饰指针
- const修饰指针—常量指针
- const修饰常量—指针常量
- const既修饰指针,又修饰常量
const修饰的是指针,指针指向可以改,指针指向的值不可以改
const int* p1 = &a;
const修饰的是常量,指针指向不可以改,指针指向的值可以更改
int* const p2 = &a;
const既修饰指针,又修饰常量,指针的指向和指针指向的值都不可以改变
const int* const p = &a;
结构体
struct 结构体名称{ 结构体成员列表};
struct 结构体名 数组名[元素个数]= {{},{}...{}};
结构体指针
利用操作符->可以通过结构体指针访问结构体属性
struct Student s1;
struct Student* p = &s1;
p->score = 10;
内存分区模型
- 代码区:存放函数的二进制代码,由操作系统进行管理的
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
- 堆区: 由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
内存四区意义:不同区域存放的数据,赋予不同的声明周期,给我们更大的灵活编程
程序运行前
代码区:
存放cpu执行的机器指令
代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份即可
代码区是只读的,使其只读的原因是防止程序意外的修改了它的指令
全局区:
全局变量和静态变量存放于此
全局区还包含了常量区,字符串常量和其他常量也存放于此
该区域的数据在程序结束之后由操作系统释放
程序运行后
栈区:
- 由编译器自动分配释放,存放函数的参数值,局部变量等。
- 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
堆区:
- 由程序员分配释放,若程序员不释放,程序结束之后有操作系统回收
- 在C++中主要利用new在堆区中开辟内存
new运算符
int* p = new int(10);//分配一个整型,值为10,p指向它
int* arry = new int[10];//分配一个int的数组;p指向第一个int
delete p;//p必须指向一个动态分配的对象或为空
delete[] arry;//arry必须指向一个动态分配的数组或为空
引用
作用:给变量起别名
语法:数据类型 &别名 = 原名
- 引用必须初始化——告诉它它是谁的别名
- 引用在初始化之后,不可以改变
做函数参数
作用:函数传参时,可以利用引用让形参修饰实参
优点:可以简化指针修改实参(传址)。
此时形参就是实参的一个别名
做函数的返回值
注意:不要返回局部变量引用
用法:函数调用作为左值
int& test1()
{
int a = 10;//栈区
return a;
}
int& test2()
{
static int b = 20;//静态变量存放在全局区,全局区的数据在程序结束后系统释放
return b;
}
int main(void)
{
int& ret = test1();
int& ret2 = test2();
cout << ret2 << endl;
//作为左值
test2() = 1000;//如果函数的返回值是引用,这个函数调用可以作为左值
cout << ret2 << endl;
cout << ret << endl;//第一次结果正确是因为编译器做了保留
cout << ret;//第二次结果错误是因为a的内存已经释放
引用本质
本质:引用的本质在c++内部实现是一个指针常量,引用一旦被初始化之后就不能更改。
int a = 10;
int &ref = a;//自动转化int* const ref = &a;//指针常量是指针指向不可改,也说明为什么引用不可更改
ref =20;//自动发现ref是引用,自动转换为*ref = 20;
常量引用
作用:常量引用主要用来修饰形参,防止误操作
const int& ref = 10;
//加上const之后,编译器将代码修改为
int temp =10;
int& ref = temp;
//加入const之后变为只读不可以修改
在函数形参列表中,可以加const修饰形参,防止形参改变实参
void showvaL(const int& ref){ }
函数提高
函数默认参数
在c++中函数形参列表中的形参是可以有默认值的。
语法:返回值类型 函数名(参数=默认值)
注意事项:如果某个位置已经有了默认参数,那么从这个位置往后都要有默认参数
声明和实现只能有一个有默认参数。
函数占位参数
函数的形参列表里可以有占位参数,用来占位,调用函数的时候必须填补该位置。
语法:返回值类型 函数名 (数据类型){}
void func(int a, int)
{}
还可以有默认参数
void func(int a, int =10)
{}
函数重载
作用:函数名可以相同,提高复用性
满足条件
- 同一个作用域下
- 函数名相同
- 函数参数类型不同或者个数不同或者顺序不同
注意:函数的返回值不可以作为函数重载的条件
函数重载的注意事项
- 引用作为函数重载条件
void func(int &a)
void func(const int &a)
- 函数重载碰到函数默认参数
类和对象
C++面向对象的三大特性为:封装、继承、多态。
C++认为万事万物皆为对象,对象上有其属性和行为
封装
封装的意义
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
- public——公共权限——类内成员可以访问,类外可以访问
- protected–保护权限——类内和派生类成员可以访问,类外不可以访问
- private——私有权限——只有类内成员可以访问,类外不可以访问
友元可访问。
在C++中struct和class的唯一区别就是默认的访问权限不同。
- struct默认权限为公共public
- class默认权限为私有private
构造函数和析构函数
会被编译器自动调用,完成对象初始化和清理工作。
编译器会提供,但是编译器提供的构造函数和析构函数是空实现。
- 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
- 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
构造函数语法
类名(){} 如: Person(){}
- 构造函数没有返回值也不写void
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载
- 程序在调用对象的时候会自动调用构造,无须手动调用,而且只会调用一次
析构函数语法
~类名(){} 如:~Person(){}
在main函数中析构函数会在按完任意键之后被调用
构造函数的分类及调用
两种分类方式:
- 按参数分为:有参构造和无参构造
- 按类型分为:普通构造和拷贝构造
三种调用方式:
- 括号法 Person p;
- Person p2(10);//有参构造函数调用;
- Person p3(p2);//拷贝构造函数调用
使用默认构造函数的时候,不要加(),编译器会认为这是一个函数的声明:
Person p1();
- 显示法 Person p1;
- Person p2 = Person(10);//有参
- Person p3 = Person(p2);//拷贝
Person(10)这是一个匿名对象-特点:当前行执行结束后,系统会立即回收掉匿名对象
不要利用拷贝函数初始化匿名对象:Person(p3)等价于Person p3
- 隐式转换法
Person p4 = 10;//相当与Person p4 = Person(10);
Person p5 = p4;//拷贝构造
拷贝构造函数调用时机
- 使用一个已经创建完毕的对象来初始化一个新对象
Person p2(p1);
- 值传递的方式给函数参数传值
void dowork(Person p){}
dowork(p);
- 以值方式返回局部对象
Person dowork2(){
Person p1;
return p1;}
Person p = dowork2();
构造函数的调用规则
- 如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,C++不会再提供其他构造函数
深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作。
深拷贝:在堆区中重新申请空间,进行拷贝操作。
浅拷贝带来的问题——内存重复释放。(动态分配内存时
自己实现拷贝构造函数来解决浅拷贝带来的问题
解决:重新在堆区找一块内存来存放他。
深拷贝——手动创建拷贝构造函数。
Person(const Person& p)
{//m_Height = p.m_Height;编译器默认实现的就是这行代码
//深拷贝操作
m_Height = new int(*p.m_Height);}
如果有属性在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。
初始化列表
构造函数():属性1(值1),属性2(值2)…{}
Person(int a,int b,int c) :m_A(a), m_B(b), m_C(c)
类对象作为类成员
当其他类的对象作为本类的成员时,构造时先构造其他类的对象,再构造自身。
自身的析构函数先进行,之后其它类再进行。
静态成员
静态成员就是在成员变量和成员函数前面加上关键字static,称为静态成员。(也是有访问权限的
- 静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
- static int age; int Person::age = 10;
- 静态成员函数
- 所有成员共享同一个函数
- 静态成员函数只能访问静态成员变量
- static void func(){}
- p.func(); Person::func();
C++对象模型和this指针
类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。(算进类的大小中
空对象的大小是1,为的是区分不同对象在内存中的占用位置。
注意非静态成员变量不初始化也会分配内存
this指针的概念
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会公用一块代码。
这一块代码是如何区分是哪个对象调用自己的呢?
this指针指向被调用的成员函数所属的对象。
是隐含在每个非静态成员函数内的一种指针。(静态没有this
- 当形参和成员变量同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this
空指针返回成员函数
C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针,如果用到this指针,需要加以判断来保证代码的健壮性。
const修饰成员函数
常函数:
- 成员函数后加const后我们称这个函数为常函数
- 常函数不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
- void showPerson() const{}
常对象:
- 声明对象前const称该对象为常对象。
- 常对象只能调用常函数。
- const Person p;
函数前const:普通函数或成员函数(非静态成员函数)前均可加const修饰,表示函数的返回值为const,不可修改。
函数后加const:只有类的非静态成员函数后可以加const修饰,表示该类的this指针为const类型,不能改变类的成员变量的值,即成员变量为read only(例外情况见2),任何改变成员变量的行为均为非法。此类型的函数可称为只读成员函数。
类中const(函数后面加)与static不能同时修饰成员函数,原因有以下两点①C++编译器在实现const的成员函数时,为了确保该函数不能修改类的实例状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的,也就是说此时const的用法和static是冲突的;
②两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系,因此不能同时用它们。
友元
让一个函数或者类 访问另一个类中的私有元素。
关键字——friend
全局函数做友元
就是将此函数在类的最上面写一个声明,前面加一个friend。
class Building
{
friend void goodgay(Building* building);
}
void goodgay(Building* building){}
类做友元
一个类在另一个中friend class xx。
class Building;
class GoodGay
{};
class Building
{
//GoodGay是Building类的好朋友,可以访问其私有属性
friend class GoodGay;
};
成员函数做友元
告诉编译器 另一个类中的xx成员函数作为本类的好朋友,可以访问私有函数。
class Building;
class GoodGay
{
void visit();//可以访问Building中私有成员
};
class Building
{
//告诉编译器 GoodGay类中的visit成员函数作为本类的好朋友,可以访问私有函数
friend void GoodGay::visit();
};
运算符重载
加号运算符重载
作用:实现两个自定义数据类型相加的运算。
1.成员函数重载+
Person operator+(Person& p){}
2.全局函数重载+
Person operator+(Person& p1, Person& p2){}
3.全局函数重载+的重载
Person operator+(Person& p1, int num){}
内置的数据类型的表达式的运算符是不可能改变的
左移运算符重载
作用:可以输出自定义的类型
ostream& operator<<(ostream &cout, Person &p)
{
cout << p.m_A << endl;
cout << p.m_B << endl;
return cout;
}
//这样写的本质就是operator<<(cout,p)简化版本就是cout<<p;
cout << p;
类内实现重载左移运算符:
void operator<<(ostream &cout)
{
cout << p.m_A << endl;
cout << p.m_B << endl;
}
p<<cout
//利用成员函数重载左移运算符p.operator<<(cout)简化版本p<<cout
//一般我们不会利用成员函数来重载<<运算符,因为无法实现cout在左边
递增运算符重载
//重载++运算符——前置
//返回引用是为了一直对一个数据进行递增操作
class MyInteger{
MyInteger& operator++(){
++m_Num;
return *this;}
}
//重载++运算符——后置
MyInteger operator++(int)
//这个int在这里作为占位参数,用来区分前置递增和后置递增
{
MyInteger temp = *this;
m_Num++;
return temp;
//后置递增要返回值,因为如果返回引用,这里相当于返回的是一个局部对象的引用。
//局部对象在当前函数执行完毕之后就被释放掉了,还要返回引用就是非法操作。
}
myint.operator++() 简写为:++myint
myint.operator++(int) 简写为:myint++
前置递增返回引用,后置递增返回值。
赋值运算符重载
//重载赋值运算符
Person& operator=(Person &p)
{
//编译器默认提供的是浅拷贝操作
//m_Age = p.m_Age;
//应该先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝。
if (m_Age != NULL)
{
delete m_Age;
m_Age = NULL;
}
//深拷贝操作
m_Age = new int(*p.m_Age);
return *this;
}
关系运算符重载
作用:重载关系运算符,可以让两个自定义类型对象进行对比操作
函数调用运算符重载
也称为仿函数
void operator()(string text) {cout << text << endl;}
myprint("hello world");
int operator()(int a, int b) {return a + b;}
MyAdd myadd;
cout << myadd(1, 2) << endl;
//匿名函数对象——特点:当前行被执行完立即释放
cout << MyAdd()(100,100) << endl;
继承 减少重复代码量
class 子类:继承方式 父类
终极原则:私有成员永远只能被本类的成员函数访问,或者是友元访问,除此以外,即使是其派生出的子类,也不能直接访问这些私有成员。
派生类中的成员函数可以直接访问基类的公有和保护成员,但不能访问基类的私有成员。
父类中所有的非静态成员属性都会被子类继承下去。
父类中私有的成员属性是被编译器给隐藏了,因此访问不到
继承中构造和析构的顺序
先构造父类,成员对象,再构造子类
先析构子类,成员对象,再析构父类
继承同名成员处理方式
//如果要通过子类对象访问到父类中的同名成员,需要加作用域。
cout <<son.Base::m_A<< endl;
继承同名静态成员处理方式
静态成员和非静态成员出现同名,处理方式 一致
//通过对象访问
cout << son1.m_A << endl;
cout << son1.Base::m_A << endl;
//通过类名访问
cout << Son::m_A << endl;
//第一个::代表通过类名方式访问,第二个::代表访问父类作用域下
cout << Son::Base::m_A << endl;
多继承语法
class 子类:继承方式 父类1,继承方式 父类2
菱形继承
vbptr——虚基类
- 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义。
- 利用虚继承可以解决菱形继承问题——virtual
父类:virtual void speak(){}
子类:void speak(){}
多态
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
区别
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
多态满足条件
- 有继承关系
- 子类重写父类中的虚函数
多态的使用条件
- 父类指针或引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
虚函数表
表内记录一个虚函数的地址
当子类重写父类的虚函数后,子类中的虚函数表内部会替换成子类的虚函数地址。
多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展以及维护
纯虚函数和抽象类
virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public:
virtual void func() = 0;
};
class Son :public Base
{
public:
void func()
{}
};
虚析构和纯虚析构
多态使用的时候,如果子类中有属性开辟到堆区,那么父类指针在释放的时无法调用到子类的析构代码
解决方法:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯析构共性:
可以解决父类指针释放子类对象,
都需要有具体的函数实现
虚析构和纯虚构的区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象
//虚析构
virtual ~类名(){}
//纯虚析构,需要声明也需要实现
virtual ~类名() = 0;//声明
类名::~类名(){}
文件操作
头文件< Fstream>
- ofstream:写操作
- ifstream:读操作
- fstream:读写操作
写文件
- 包含头文件——#include< fstream>
- 创建流对象——ofstream ofs;
- 打开文件——ofs.open(“文件路径”,打开方式)
如:ofstream ofs("person.txt", ios::out | ios::binary);
- 写数据——ofs<<“写入的数据”;
- 关闭文件——ofs.close();
读文件
- 包含头文件——#include< fstream>
- 创建流对象——ifstream ifs;
- 打开文件并判断文件是否打开成功——ifs.open(“文件路径”,打开方式);
- 读数据——四种方式读取
//第一种
/*char buf[1024] = { 0 };
while (ifs>>buf)
{
cout << buf << endl;
}*/
//第二种
/*char buf[1024] = { 0 };
while (ifs.getline(buf,sizeof(buf)))
{
cout << buf << endl;
}*/
//第三种
/*string buf;
while (getline(ifs,buf))
{
cout << buf << endl;
}*/
//第四种-不推荐
char c;
while ((c = ifs.get()) != EOF)//EOF——end of file
{
cout << c;
}
- 关闭文件——ifs.close();
二进制文件
打开方式主要为ios::binary
写文件
二进制方式写文件主要利用流对象调用成员函数write
ostream& wirte(const char* buffer,int len);
字符指针buffer指向内存中一段存储空间。len是读写的字节数
Person p = { "张三",18 };
ofs.write((const char*)&p,sizeof(Person));
读文件
二进制方式读文件主要利用流对象调用成员函数read
istream& read(char * buffer,int len);
字符指针buffer指向内存中一段存储空间。len是读写的字节数
Person p;
ifs.read((char*)&p, sizeof(Person));
cout << "姓名:" << p.m_Name<<" " << "年龄:" << p.m_Age << endl;
难点强化
Q:系统大小端是什么?如何检测
A: 大端是低地址存高位字节, 小端是低地址存放低位字节。网络传输都是采用大字节,检测:将整型和字符放在一个联合体里,令int=1
32位int型数0x12345678
程序编译流程(here
A: 预处理(预处理如 #include、#define 等预编译指令,生成 .i 或 .ii 文件)
编译(编译器进行词法分析、语法分析、语义分析、中间代码生成、目标代码生成、优化,生成 .s 文件)
汇编(汇编器把汇编码翻译成机器码,生成 .o 文件)
链接(链接器对未分配的变量分配绝对地址, 把各个模块之间相互引用部分处理好,地址对应上。 最后生成 .out 文件)
const
修饰变量,指针,引用,成员函数
不存在引用常量,因为引用只是对象的别名,引用不是对象,不能用 const 修饰
static
修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
修饰成员函数 static 函数内不能访问非静态成员
this 指针
当对一个对象调用成员函数时,编译程序先将对象的地址赋给 this
指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用 this
指针。
不能给 this
指针赋值;const
成员函数中不能对this
的数据成员进行赋值操作
不是一个常规变量,是个右值,所以不能取得 this
的地址
场景:对象的链式引用;避免对同一对象进行赋值操作;实现一些数据结构时,如 list
。
inline 内联函数
相当于把内联函数里面的内容写在调用内联函数处;不用执行进入函数的步骤,直接执行函数体;比宏多了类型检查,真正具有函数特性;不内联包含循环、递归、switch 等复杂操作的内联函数;在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数;类外定义,需要显式内联
处理步骤
- 将 inline 函数体复制到 inline 函数调用点处;
- 为所用 inline 函数中的局部变量分配内存空间;
- 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
- 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。
优点:省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。做安全检查或自动类型转换;可以访问类的成员变量,宏定义则不能;在运行时可调试,而宏定义不可以。
缺点:代码膨胀;无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
inline virtual
唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如Base::who()
),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
volatile
用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
- const 可以是 volatile (如只读的状态寄存器)
- 指针可以是 volatile
assert()
断言,是宏,而非函数。
如果它的条件返回错误,则终止程序执行
通过定义 NDEBUG
来关闭 assert,但是需要在源代码的开头,include <assert.h>
之前。
#defineNDEBUG// 加上这行,则 assert 不可用
#include<assert.h>
sizeof()
- sizeof 对数组,得到整个数组所占空间大小。
- sizeof 对指针,得到指针本身所占空间大小。
#pragma pack(n)
设定结构体、联合以及类成员变量以 n 字节方式对齐
位域
类可以将其(非静态)数据成员定义为位域(bit-field),在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。
- 位域在内存中的布局是与机器有关的
- 位域的类型必须是整型或枚举类型,带符号类型中的位域的行为将因具体实现而定
- 取地址运算符(&)不能作用于位域,任何指针都无法指向类的位域
extern "C"
让 C++ 编译器将 extern "C"
声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。
struct 和 typedef struct
typedefstructStudent {
intage;
} S;
等价于
structStudent {
intage;
};
typedefstructStudentS;
S
等价于 struct Student
,但两个标识符名称空间不相同。
还可以定义与 struct Student
不冲突的 void Student() {}
。
C++ 中
由于编译器定位符号的规则(搜索规则)改变,导致不同于C语言。
一、如果在类标识符空间定义了 struct Student {...};
,使用 Student me;
时,编译器将搜索全局标识符表,Student
未找到,则在类标识符内搜索。
即表现为可以使用 Student
也可以使用 struct Student
二、若定义了与 Student
同名函数之后,则 Student
只代表函数,不代表结构体
typedefstructStudent {
int age;
} S; //void S() {} // 错误,符号 "S" 已经被定义为一个 "struct Student" 的别名
struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
默认的访问控制不同
union 联合
联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。
特点:
- 默认访问控制符为 public
- 可以含有构造函数、析构函数
- 不能含有引用类型的成员
- 不能继承自其他类,不能作为基类
- 不能含有虚函数
- 匿名 union 在定义所在作用域可直接访问 union 成员
- 匿名 union 不能包含 protected 成员或 private 成员
- 全局匿名联合必须是静态(static)的
C 实现 C++ 类
C 实现 C++ 的面向对象特性(封装、继承、多态)
- 封装:使用函数指针把属性与方法封装到结构体中
- 继承:结构体嵌套
- 多态:父类与子类方法的函数指针不同
explicit(显式)关键字
- explicit 修饰构造函数时,可以防止隐式转换和复制初始化
- explicit 修饰转换函数时,可以防止隐式转换,但 按语境转换 除外
using
在 C++11 中,派生类能够直接重用其基类定义的构造函数。
classDerived : Base {
public:using Base::Base;
/* ... */
};
using 指示
使得某个特定命名空间中所有名字都可见,无需再为它们添加任何前缀限定符
尽量少使用 using 指示
污染命名空间
:: 范围解析运算符
全局作用域符,表示作用域为全局命名空间
类作用域符,指定类型的作用域范围是具体某个类的
命名空间作用域符
表示指定类型的作用域范围是具体某个命名空间的
enum 枚举类型
限定作用域的枚举类型
enum class open_modes { input, output, append };
不限定作用域的枚举类型
enum color { red, yellow, green };
enum { floatPrec = 6, doublePrec = 10 };
decltype
用于检查实体的声明类型或表达式的类型及值分类
尾置返回允许我们在参数列表之后声明返回类型
引用
右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用
实现转移语义和精确传递
- 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
- 能够更简洁明确地定义泛型函数。
成员初始化列表
好处
- 更高效:少了一次调用默认构造函数的过程。
- 有些场合必须要用初始化列表:
- 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
- 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
- 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化
多态
- 重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
- 子类型多态(Subtype Polymorphism,运行期):虚函数
- 参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
- 强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换
注意:
- 可以将派生类的对象赋值给基类的指针或引用,反之不可
- 普通函数(非类成员函数)不能是虚函数
- 静态函数(static)不能是虚函数
- 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)
- 内联函数不能是表现多态性时的虚函数
虚析构函数
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
虚函数、纯虚函数
让这个函数在它的子类里面可以被覆盖(override),编译器就可以使用后期绑定来达到多态
虚函数指针、虚函数表
- 虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。
- 虚函数表:在程序只读数据段(
.rodata section
,见:目标文件存储结构),存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。
虚继承
虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。
底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
虚继承、虚函数
- 不同之处:
- 虚继承
- 虚基类依旧存在继承类中,只占用存储空间
- 虚基类表存储的是虚基类相对直接继承类的偏移
- 虚函数
- 虚函数不占用存储空间
- 虚函数表存储的是虚函数地址
- 虚继承
类模板、成员模板、虚函数
- 类模板中可以使用虚函数
- 一个类(无论是普通类还是类模板)的成员模板(本身是模板的成员函数)不能是虚函数
抽象类、接口类、聚合类
- 抽象类:含有纯虚函数的类
- 接口类:仅含有纯虚函数的抽象类
- 聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下特点:
- 所有成员都是 public
- 没有定义任何构造函数
- 没有类内初始化
- 没有基类,也没有 virtual 函数
内存分配和管理
malloc、calloc、realloc、alloca
- malloc:申请指定字节数的内存。申请到的内存中的初始值不确定。
- calloc:为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为 0。
- realloc:更改以前分配的内存长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定。
- alloca:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca 不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca 不宜使用在必须广泛移植的程序中。C99 中支持变长数组 (VLA),可以用来替代 alloca。
malloc、free
用于分配、释放内存
malloc、free 使用
申请内存,确认是否申请成功
char *str = (char*) malloc(100);
new、delete
- new / new[]:完成两件事,先底层调用 malloc 分配了内存,然后调用构造函数(创建对象)。
- delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用 free 释放空间。
- new 在申请内存时会自动计算所需字节数,而 malloc 则需我们自己输入申请内存空间的字节数。
定位 new
定位 new(placement new)允许我们向 new 传递额外的地址参数,从而在预先指定的内存区域创建对象。
delete this 合法吗?
合法,但:
- 必须保证 this 对象是通过
new
(不是new[]
、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的 - 必须保证调用
delete this
的成员函数是最后一个调用 this 的成员函数 - 必须保证成员函数的
delete this
后面没有调用 this 了 - 必须保证
delete this
后没有人使用了
如何定义一个只能在堆上(栈上)生成对象的类?
只能在堆上
方法:将析构函数设置为私有
原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
只能在栈上
方法:将 new 和 delete 重载为私有
原因:在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。
智能指针
- shared_ptr实现共享式拥有(shared ownership)概念。多个智能指针指向相同对象,该对象和其相关资源会在 “最后一个 reference 被销毁” 时被释放。
支持定制型删除器(custom deleter),可防范 Cross-DLL 问题(对象在动态链接库(DLL)中被 new 创建,却在另一个 DLL 内被 delete 销毁)、自动解除互斥锁
- unique_ptr实现独占式拥有(exclusive ownership)或严格拥有(strict ownership)概念,保证同一时间内只有一个智能指针可以指向该对象。可以移交拥有权。它对于避免内存泄漏(resource leak)——如 new 后忘记 delete ——特别有用。
- weak_ptr允许你共享但不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何 weak_ptr 都会自动成空(empty)。可打破环状引用(cycles of references,两个其实已经没有被使用的对象彼此互指,使之看似还在 “被使用” 的状态)的问题
- auto_ptr(被 C++11 弃用)缺乏语言特性如 “针对构造和赋值” 的
std::move
语义
auto_ptr 与 unique_ptr 比较
- auto_ptr 可以赋值拷贝,复制拷贝后所有权转移;unique_ptr 无拷贝赋值语义,但实现了
move
语义; - auto_ptr 对象不能管理数组(析构调用
delete
),unique_ptr 可以管理数组(析构调用delete[]
);
强制类型转换运算符
static_cast
- 用于非多态类型的转换
- 不执行运行时类型检查(转换安全性不如 dynamic_cast)
- 通常用于转换数值数据类型(如 float -> int)
- 可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类可能有不在父类的字段或方法)
向上转换是一种隐式转换。
dynamic_cast
- 用于多态类型的转换
- 执行行运行时类型检查
- 只适用于指针或引用
- 对不明确的指针的转换将失败(返回 nullptr),但不引发异常
- 可以在整个类层次结构中移动指针,包括向上转换、向下转换
const_cast
- 用于删除 const、volatile 和 __unaligned 特性(如将 const int 类型转换为 int 类型 )
reinterpret_cast
- 用于位的简单重新解释
- 滥用 reinterpret_cast 运算符可能很容易带来风险。 除非所需转换本身是低级别的,否则应使用其他强制转换运算符之一。
- 允许将任何指针转换为任何其他指针类型(如
char*
到int*
或One_class*
到Unrelated_class*
之类的转换,但其本身并不安全) - 也允许将任何整数类型转换为任何指针类型以及反向转换。
- reinterpret_cast 运算符不能丢掉 const、volatile 或 __unaligned 特性。
- reinterpret_cast 的一个实际用途是在哈希函数中,即,通过让两个不同的值几乎不以相同的索引结尾的方式将值映射到索引。
bad_cast
- 由于强制转换为引用类型失败,dynamic_cast 运算符引发 bad_cast 异常。
运行时类型信息 (RTTI)
typeid
- typeid 运算符允许在运行时确定对象的类型
- type_id 返回一个 type_info 对象的引用
- 如果想通过基类的指针获得派生类的数据类型,基类必须带有虚函数
- 只能获取对象的实际类型
type_info
- type_info 类描述编译器在程序中生成的类型信息。 此类的对象可以有效存储指向类型的名称的指针。 type_info 类还可存储适合比较两个类型是否相等或比较其排列顺序的编码值。 类型的编码规则和排列顺序是未指定的,并且可能因程序而异。
容器 | 底层数据结构 | 时间复杂度 | 有无序 | 可不可重复 | 其他 |
数组 | 随机读改 O(1) | 无序 | 可重复 | 支持随机访问 | |
数组 | 随机读改、尾部插入、尾部删除 O(1) 头部插入、头部删除 O(n) | 无序 | 可重复 | 支持随机访问 | |
双端队列 | 头尾插入、头尾删除 O(1) | 无序 | 可重复 | 一个中央控制器 + 多个缓冲区,支持首尾快速增删,支持随机访问 | |
单向链表 | 插入、删除 O(1) | 无序 | 可重复 | 不支持随机访问 | |
双向链表 | 插入、删除 O(1) | 无序 | 可重复 | 不支持随机访问 | |
deque / list | 顶部插入、顶部删除 O(1) | 无序 | 可重复 | deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 | |
deque / list | 尾部插入、头部删除 O(1) | 无序 | 可重复 | deque 或 list 封闭头端开口,不用 vector 的原因应该是容量大小有限制,扩容耗时 | |
vector + max-heap | 插入、删除 O(log2n) | 有序 | 可重复 | vector容器+heap处理规则 | |
红黑树 | 插入、删除、查找 O(log2n) | 有序 | 不可重复 | ||
红黑树 | 插入、删除、查找 O(log2n) | 有序 | 可重复 | ||
红黑树 | 插入、删除、查找 O(log2n) | 有序 | 不可重复 | ||
红黑树 | 插入、删除、查找 O(log2n) | 有序 | 可重复 | ||
哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 不可重复 | ||
哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 可重复 | ||
哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 不可重复 | ||
哈希表 | 插入、删除、查找 O(1) 最差 O(n) | 无序 | 可重复 |