C++基础知识复习
第一部分:基础知识
一、const
1. 作用
-
修饰变量,表示不可能更改
-
修饰指针
const int *ptr
——pointer to constint const *ptr
—— const pointer- 原则:被const修饰的后面的值是不可改变的
-
修饰引用
常用于形参。即避免了copy,又避免了对其值的修改
-
修饰成员函数
表示该成员函数不能修改成员变量
二、static
1. 作用
-
修饰普通变量
修改变量的存储区域,使其存储在
静态区。
在main函数执行前就分配了空间;
如果没有被显式初始化,则用默认值初始化
-
修饰普通函数
表明函数的作用范围。
仅在定义该函数的文件内才能使用(多人项目中,为了防止与其他namespace重名,可以将函数定义为static)
-
修饰成员变量
使其变为类变量,即所有instance公用一个变量
-
修饰成员函数
使得不需要构造对象就可以访问此函数
static函数内不能访问非静态成员(毕竟人家不要构造实例)
三、this指针
this
指针是一个隐含于每一个非静态成员函数中的特殊指针,指向调用该成员函数的那个对象。(类似ptyhon类中的self
)- 当对一个对象调用成员函数时,程序先讲对象的地址赋给
this
指针,然后调用成员函数;每次成员函数存取数据变量时,都隐式地使用this
指针 this
指针被隐式地声明为ClassName *const this
——意味着不能给this
指针赋值;在ClassName
类的const
成员函数中,this
指针的类型为:const ClassName* const
,这说明不能修改this
指针指向的数据成员。this
并不是一个常规变量,而是一个右值,所以不能取得this
的地址(能不能&this
)- 以下场景中,经常显式的引用
this
指针:- 为实现对象的链式引用
- 为避免对同一对象进行赋值操作
- 为实现一些数据结构时,如
list
四、inline内连函数
1. 特点
- 编译时,将内联函数内容在调用处展开
- 省略了进入函数的步骤,直接执行函数体
- 类似于
宏
,但多了类型检查,具有函数特性 - 编译器一般不inline包含循环、递归、switch等复杂操作的函数
- 类中定义的函数,除了虚函数之外,其他函数都会自动隐式地的当成内联函数。
2. 编译器对inline函数的处理步骤
- 将
inline
函数体复制到inline函数调用处 - 为所有
inline
函数中的局部变量分配内存空间 - 将
inline
函数的输入参数和返回值映射到调用方法的局部变量中 - 如果
inline
函数包括多个返回点,将其转变为inline
函数代码块末尾的分支(使用GOTO)
3. 优点
- 省去了参数压栈、栈帧开辟与回收,结果返回的开销
- 相比与
宏
来讲,在代码展开时,会做安全检查
或者自动类型转换
- 在类中声明同时定义的成员函数,会自动
inline
,因此内联函数可以访问类的成员变量,但宏
不可以 - 内联函数在运行时可调试,宏定义不可以
4. 缺点
- 代码膨胀。若执行函数体的时间比函数调用大很多,则
inline
收益很小。 inline
无法随着函数库升级而升级。inline
函数的改变需要重新编译,而non-inline
可以直接连接- 是否内联,程序员不可控。
inline
函数只是对编译器的建议,是否内联,由编译器决定。
5. 虚函数可以是inline
吗
- 可以。但当虚函数表现多态时的时候,不能内联
- 内联发生在编译时,而虚函数的多态性是在运行期,编译器无法知道运行时调用哪个函数。因此虚函数表现为多态时(运行期)不能内联
inline virtual
唯一可以内联:编译器知道要调用的对象是哪个类(如Base::who()
),这只发生在编译器具有实际对象,而非对象的指针或引用。
例子:
#include <iostream>
using namespace std;
class Base {
public:
inline virtual void who(){cout << "Base::who"<<endl;}
virtual ~Base()}{}
}
class Derived : public Base {
public:
void who(){cout << "Derived::who"<< endl;}
}
int main(){
Base b;
b.who(); // 此处编译其就知道要调用哪个函数
Base *ptr = new Derived();
ptr->who();// 多态
delete ptr;
ptr = nullptr;
return 0;
}
五、sizeof
- 对数组,返回整个数组占用的空间大小
- 对指针,返回指针本身占用的空间大小
六、extern "C"
- 被
extern
的函数或变量是extern类型,跨文件访问 - 被修饰的变量或函数是按照C语言方式编译和链接的
#ifdef __cplusplus
extern "C"{
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif
七、union联合
1. 概念
可以有多个数据成员,但在任意时刻只有一个数据成员可以有值。当某个成员被赋值后,其他成员变成未定义状态。
2. 特点
- 默认为public
- 可以有构造函数、析构函数
- 不能含有引用类型成员
- 不能继承自其他类,不能作为基类
- 不能包含虚函数
- 匿名union在定义所在的作用域可以直接访问union成员
- 匿名union不能包含
protected
和privated
成员 - 全局匿名union必须是静态的
#include <iostream>
using namespace std;
union UnionTest {
UnionTest():i(10){}; // 构造函数
int i;
double d;
};
static union{ // 全局静态匿名union
int i;
double d;
};
int main(){
UnionTest u;
cout << u.i << u.d << endl;
Union { // 局部匿名union
int i;
double d;
};
::i = 20; // 全局静态union.i值
i = 30;// 局部i
return 0;
}
八、C实现C++类
-
封装
使用函数指针把属性和方法封装在结构体中
-
继承
结构体嵌套
-
多态
父类与子类方法的函数指针不同
九、explicit
- 修饰构造函数是,防止隐式转换和复制初始化
- 修饰转换函数时,防止隐式转换,但按语境转换除外
十、friend友元类和友元函数
- 能访问私有成员
- 破坏了封装性
- 友元函数不可传递
- 友元函数的单向性
- 友元声明的形式和数量不受限制
十一、using
1. 引入命名空间的一个成员
using namespace_name::name;
2. C++11中派生类重用基类的构造函数
class Derived : public Base{
public:
using Base::Base;
};
对于基类的每个构造函数,编译器都会生成一个与之对应(形参类列表完全相同)的派生类构造函数。
3. using指示
using namespace std;// 无需为std里的所有名字添加std前缀了
注:
- 应尽量少用
using 指示
,会污染命名空间- 如果只引入一个成员,且与局部名称冲突了,编译器会发出指示
- 但如果全导入了,且覆盖了局部名称,编译器不会提示,排查问题较难
十二、::范围解析符
1. 类别
-
::name
全局作用符。用于类型名称(如类、类成员、成员函数、变量)前。
-
class::name
类作用域符。用于指定类型的作用域范围是具体某个类的。
-
namespace::name
命名空间作用域符用于指定类型的作用域范围是某个命名空间的。
int count = 1; // ::count
class A{
public:
static int count = 2; // A::count
};
void foo(){
int count = 3;
}
十三、引用
1. 左值引用
常规引用,表示对象的身份。
2. 右值引用
右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。
右值引用可以实现转移语义(move sementics)和精确传递(perfect forwarding),主要目的有两个:
- 消除两个对象交互时不必要的对象拷贝,节省存储资源,提升效率
- 能够更简洁明确地定义泛型函数
3. 引用折叠
X& &
、X& &&
、X&& &
可以折叠为X&
X&& &&
可以折叠为X&&
十四、成员初始化列表
1. 优点
-
更高效
少了一次调用默认构造函数的过程
-
有些场合必须要初始化列表
-
常量成员
因为常量只能初始化,不能赋值,所以必须放在初始化列表中
-
引用类型
引用必须在定义时初始化,并不能重新赋值
-
没有默认构造函数的类类型
因为使用初始化列表可以不必调用默认构造函数,就可初始化
-
2. initializer_list
用花括号初始化器初始化一个列表,其中构造函数接受一个std::initializer_list
参数
#include <iostream>
#include <vector>
#include <intializer_list>
template<typename T>
struct S{
std::vector<T> v;
S(std::initializer_list<T> l): v(l){
std::cout << "init"<< endl;
}
void append(std::initializer_list<T> l){
v.insert(v.end(), l.begin(), l.end());
}
std::pair<const T*, std::size_t> c_arr() const {
return {&v[0], v.size()};
}
};
第二部分:面向对象
一、多态
1. 概念
-
多态,可以理解为消息以多种形式显示的能力
-
多态是以封装和继承为基础的
-
C++多态分类和实现
-
重载多态(Ad-hoc,编译期)
函数重载,运算符重载
-
子类型多态(subtype,运行期)
虚函数
-
参数多态性(parametric,编译期)
类模板、函数模板
-
强制多态(coercion,编译器/运行期)
基本类型转换、自定义类型转换
-
2. 静态多态
编译期,早绑定
函数重载
class A{
void foo();
void foo(int a);
};
3. 动态多态
运行期/晚绑定
虚函数:virtual
修饰
-
普通函数(非类成员函数)不能是虚函数
-
静态函数(static)不能是虚函数
-
构造函数不能是虚函数
因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成时,才会形成虚表指针(重要)
-
内联函数不能是表现多态时的虚函数
4. 虚析构函数
是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
Class Base{
public:
Base();
virtual ~Base();
};
class Derived : public Base {
public:
...
};
int main(){
Base *ptr = new Derived();
delete ptr;
ptr = nullptr;
return 0;
}
5. 纯虚函数
一种特殊的虚函数。
在基类中不能对虚函数给出有意义的实现,而是声明为纯虚函数,它的实现留给派生类去做。
Class AbstraceBase{
virtual void foo(int) = 0;
};
虚函数 vs 纯虚函数
- 类如果声明虚函数,且实现了,哪怕是空实现,则作用就是为了能让这个函数在派生类里被override。这样,编译器就可以使用后期绑定来达到多态了;纯虚函数只是一个接口,是个函数声明而已,要留到子类实现
- 虚函数在派生类可以不重写;纯虚函数必须在子类实现才可以实例化
- 虚函数的类用于实作继承,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类去完成
- 带纯虚函数的类称为抽象类,不能被实例化。只有继承并实现了纯虚函数,才能使用。派生类继承后,可以继续是抽象类,也可以是普通类。
- 虚基类是虚继承中的基类。
6. 虚函数指针、虚函数表
-
虚函数指针
在含有虚函数类的对象中,指向虚函数表,在运行时确定
-
虚函数表
在程序只读数据段
.rodata section
,存放虚函数指针。如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。
7. 虚继承
用于解决多继承条件下的菱形继承问题
浪费存储空间、存在二义性
底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现。每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用空间)(但虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr指的是虚基类表指针,指向一个虚基类表——记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持公有基类(虚基类)的两份同样拷贝,从而节省了存储空间。
虚继承 vs 虚函数
-
相同之处
都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
-
不同之处
- 虚继承
- 虚基类依旧存在继承类中,之占用存储空间
- 虚基类表存储的是虚基类的相对直接继承类的偏移
- 虚函数
- 虚函数不占用存储空间
- 虚函数表存储的是虚函数地址
- 虚继承
模板类、成员模板、虚函数
- 模板类可以使用虚函数
- 一个类的成员模板(本身是模板的成员函数)不能是虚函数
8. 抽象类、接口类、聚合类
- 抽象类:含纯虚函数
- 接口类:仅含纯虚函数的抽象类
- 聚合类:可以直接访问其成员,具有特殊的初始化语法
- 所有成员都是
public
- 没有定义任何构造函数
- 没有类内初始化
- 没有基类、没有
virtual
函数
- 所有成员都是
第三部分
一、内存分配和管理
1. malloc、calloc、realloc、alloca
-
malloc
申请指定字节数的内存。申请到的内存中的初始值不确定
-
calloc
为指定长度的对象,分配能容纳指定个数的内存。申请的内存的每一位(bit)都初始化为0
-
realloc
更改以前分配的内存长度(增加或减少)
当增加长度是,可能需要将以前分配区的内容移到另一个足够大的区域。而新增的区域内的初始值不确定
-
alloca
在栈上申请内存。
程序在出栈时,会自动释放内存。但需注意,alloca不具有可移植性,而且在没有传统堆栈的机器上很难实现。
alloca不宜使用在必须广泛移植的程序中。C99中支持变长数组(VLA),可以用来代替alloca。
2. malloc和free
分别用于内存的分配和释放
// 内存申请
char *str = (char*)malloc(100);
assert(str != nullptr);
// 内存释放
free(str);
str = nullptr;
3. new和delete
- new/new[]
- 先底层调用
malloc
分配内存 - 再调用构造函数(创建对象)
- 先底层调用
- delete/delete[]
- 先调用析构函数(清理资源)
- 再底层调用
free
释放空间
- new申请内存时会自动计算所需要的字节数;malloc则需要我们自己输入申请空间的字节数
int main(){
T* t = new T(); // 先分配内存,再构造
delete t; // 先析构,再free
return 0;
}
4. 定位new
placement new
允许我们向new
传递额外的地址参数,从而再预先指定的内存区域创建对象。
// place_addr是一个指针
// initializers提供一个以逗号分割的初始值列表
new(place_addr) type;
new(place_addr) type (initializers);
new(place_addr) type [size];
new(place_addr) type [size] {braced intializer list};
5. delete this 合法吗?
合法,但是:
- 必须保证
this
对象是通过new
(不是new[]
、不是placement new
、不是栈上、不是全局、不是其他对象成员)分配的 - 必须保证调用
delete this
的成员函数是最后一个调用this的成员函数 - 必须保证成员函数的
delete this
后面不再调用this
了 - 必须保证
delete this
后没有人再使用了
总之,delete this
对调用的成员函数有很严格的要求。
class A{
public:
A(){};
void destory(){ delete this;} // 必须显式的调用,进行内存空间释放
private:
~A(){} // 私有函数,只能通过new去动态构造
};
6. 栈上或堆上生成对象
C++中,类对象的建立有两种:
-
静态建立
-
如
A a;
-
由编译器为对象在栈空间分配内存,是通过移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数,形成一个栈对象。
-
为什么不把构造函数设置为
private
?构造函数设置为私有,会影响new动态建立对象,因为其第二阶段也会调用构造函数。
-
-
只能在
堆
上-
方法:将析构函数设置为私有
C++是静态绑定语言,编译器管理栈上对象的声明周期。
编译器为类的对象分配栈空间时。会先检查类的析构函数的访问性;
若析构函数不可访问,则不能在栈上创建对象。
-
缺点:无法解决继承问题
若其作为基类,则析构函数一般要设置为
virtual
,然后在子类重写,以实现多态,所以析构函数不能设置为private。可以将析构函数设置为protected
-
class A{
protected: // protected,解决继承问题
A(){}
~A(){}
public:
static A* create(){return new A();} // 静态方法,负责创建
void destroy() {delete this;} // 负责释放内存空间,必须最后调用
};
-
只能在
栈
上-
方法:将
new
和delete
重载为private在堆上生成对象,使用
new
关键字操作,过程包括两个阶段:- 使用
new
在堆上需找可用内存,分配给对象 - 调用构造函数生成对象
将
new
操作设置为private,那么第一阶段的操作就无法完成,就不能在堆上生成对象 - 使用
-
class A{
private:
void *operator new(size_t){}; // 函数第一个参数和返回值都是固定的
void operator delete(void* ptr){};// 重载new就需要重载delete
public:
A(){}
~A(){}
};
有个问题:
如果既把析构函数设置为private,也将
new
和delete
重载为了private,那么对象会创建在哪里?创建失败?
二、智能指针
1. 标准库
#include <memory>
std::auto_ptr<std::string> ps (new std::string(str));// C++98
// C++ 11中 auto_ptr被弃用
2. shared_ptr
多个智能指针可以共享同一个对象,对象的最末一个拥有者有责任销毁对象,并清理与该对象相关的所有资源。
优点:
-
支持定制型删除器(
custom deleter
) -
可防范
Cross-DLL
问题对象在动态链接库DLL中new创建,却在另一个DLL内被delete销毁
-
自动解除互斥锁
3. Weak_ptr
允许共享但不拥有某个对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何weak_ptr
都会自动成空。
因此,在default
和copy
构造函数之外,weak_ptr
只提供接受一个shared_ptr的构造函数
优点:
-
可打破环状引用
两个其实已经没有被使用的对象,彼此互指,使之看似还在『被使用』的状态的问题
4. unique_ptr
C++ 11提供的类型,在异常时可以帮助避免资源泄露的智能指针,用于取代auto_ptr
独占式拥有,即一个对象和相应的资源同一时间只被一个pointer
拥有。
一旦拥有者被销毁或设置为empty
,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,响应资源会被释放。
uique_ptr v.s auto_ptr
- auto_ptr可以赋值拷贝,复制拷贝后所有权转移;unique_ptr无赋值拷贝语义,但实现了
move
语义 - auto_ptr对象不能管理数组(析构调用
delete
);unique_ptr可以管理数组(析构调用delete[]
)
5. 强制类型转换
待补充