cpp
从C到C++
命名空间
命名空间用于解决代码名称冲突的问题,降低命名冲突的风险。
1 | namespace mySpace{ |
使用域解析运算符::在函数名称前给出命名空间。
1 | int main() |
也可以使用using指定命名空间或要调用的函数,其后再调用时就不需要使用命名空间了。
不要滥用using namespace std,更不要在头文件中使用using namespace std。
1 | int main() |
C++17后,命名空间可以嵌套,还可以给命名空间起别名。
1 | namespace Outer { |
字面量
字面量是写在代码中的量,包括:
- 十进制123
- 八进制0173
- 十六进制0x7B
- 二进制0b1111011
- 浮点数3.14f
- 双精度浮点值3.14
- 字符’a’
- C风格字符串”hello world”
- C++17新增的十六进制浮点数0x3.ABCp-10
数值字面量可以使用单引号作为分隔符,如23’456’789。
变量
变量的声明和初始化:
1 | int n = 7; |
C++提供的变量类型有:
- 整型
- (signed) int/signed
- unsigned (int)
- (signed) short (int)
- unsigned short (int)
- (signed) long (int)
- unsigned long (int)
- (signed) long long (int)
- unsigned long long (int)
- 浮点型
- float
- double
- long double
- 字符型
- char
- char16_t
- char32_t
- wchar_t
- bool
- std::byte(C++17)
隐式转换不必说,显式类型转换有三种方式:
1 | int i = (int)f; |
从C而来的类型
枚举的本质是整型。
1 | enum Season { |
枚举类则是类型安全的,枚举值名不会自动超出封闭的作用域,因此枚举在使用时总需要作用域解析操作符。
1 | enum class Season { |
结构体和类很相近。
1 | struct Student { |
语句
C++17允许if或switch语句使用一个初始化器,变量只能在初始化器和大括号中使用。
1 | if (int i = rand(); i % 2 == 1) { |
switch语句的表达式必须为整型或枚举,并与常量比较。C++17允许使用[[fallthrough]]有意忽略break。
1 | Season s = Season::Spring; |
函数
C++14允许函数返回类型推断。
1 | auto addNumbers(int num1, int num2) { |
函数都有一个预定义的局部变量func表示当前执行的函数名。
数组
虽然仍然可以使用C风格的数组,但是最好使用定长数组std::array和动态数组std::vector。
1 | int myArray[3] = { 1, 2, 3 }; |
结构化绑定
C++17允许用中括号同时声明多个变量,并使用数组、结构体、对组或元组来初始化。
1 | struct Point { double mX, mY; }; |
初始化表列
使用初始化表列可以编写接收可变数量参数的函数。
1 |
|
调用函数时,可以使用:
1 | int a = makeSum({ 1,2,3 }); |
指针
nullptr是空指针常量,类型是指针类型。
不要使用C的指针和malloc(),free(),使用智能指针和new,delete,new[],delete[]。
最重要的智能指针是std::unique_ptr和std::shared_ptr。
std::unique_ptr类似普通指针,但当unique_ptr超出作用域或被删除是会自动释放内存或资源,因而不需要调用delete。shared_ptr使用引用计数,超出作用域时递减引用计数,计数为0时释放对象。
1 | auto pc1 = make_unique<Complex>(); |
智能指针的reset()方法可以释放当前指针的资源并进行重设,如果不传入参数,则设为nullptr。
1 | pc1.reset(new Complex()); |
release()方法解除智能指针的所有权。
1 | Complex* p = pc1.release(); |
常量
C++的const常量是真实的常量,可以作为数组长度。
const的实现原理是符号表,编译过程中对常量直接进行替换。如果编译过程发现对常量使用&或extern,则给常量分配内存。
引用
C++的引用可以看作一个已定义变量的别名,引用的内部实现是常指针,但请把引用视作变量本身。创建引用时必须初始化,且不能修改。
1 | void func1(int& a) { a = 5; } |
函数如果返回引用,则应当将返回值视作变量本身。
1 | int a = 0; |
常量引用作为函数参数可以增加效率:函数不会创建副本,只传递指针,且不修改原始变量。
1 | void printString(const std::string& s) { |
如果要修改对象,则传递非常量引用。非常量引用的初始值必须为左值,函数此时不可以传入字面量。
三目运算符给出的是引用,因而可以做左值。
1 | a > b ? a : b = 10; |
右值引用
可以获取地址(有名称)的值称为左值,不可获取地址的称为右值。字面量、临时对象和临时值都是右值。
右值引用时对右值的引用,这用在临时对象上,在临时对象销毁前,某些值的复制可以用复制指针来代替。
1 | void fun(Complex& s) { |
要注意,右值引用作为形参是左值,因为其有地址。
move()可以将左值移动为右值。
1 | void fun(Complex&& s) { |
类型推断
类型推断有两个关键字,auto和decltype。auto可以对类型作推断,并去除const限定符和引用,有时这会产生副本,可以使用auto& 或const auto&。decltype可以把表达式作为实参,而且也不会去除const限定符和引用。
1 | decltype(foo())f = foo(); |
字符串
代码中直接出现的字符串是字符串字面量,保存在字面量池中。生字符串使用R()
或R"分隔符序列(生字符串)分隔符序列"
引导,其中不会出现转义字符。
std::string是basic_string模板类的一个实例。尽管string是一个类,但不妨把它当成一种内建类型。
string类重载了operator+,operator+=,operator==,operator!=,operator[],operator<等运算符,以符合使用者预期的方式工作。
std命名空间包含了许多辅助函数来进行string的转化,如:
- string to_string(int val);
- int stoi(const string& str, size_t* idx=0, int base=10);//idx接收第一个未能转化的字符索引
C++17引入了std::string_view类解决对参数为const string&但传入const char会创建副本的问题。*string_view是const string&的简单替代品,只包含字符串的指针和长度,从不复制字符串。通常按值传递string_view。**
1 | string_view extractExtension(string_view fileName) { |
在这个函数下,传入const char*和const string&都没有问题,也不会制作字符串的副本。只会值传递string_view,也就是指针和长度。
无法拼接string和string_view,也无法隐式地从string_view创建string。可以的方案是使用sv.data()或string(sv)。
面向对象
类与对象
对象是对数据及数据的操作方法的封装,而同类型的对象抽象出其共性就是类。类通过一个简单的外部接口与外界发生关系。对象和对象之间通过消息进行通信。
类把属性和方法进行封装,对属性和方法进行访问控制。类的访问控制关键字包括public,private和protected。类的默认访问说明符是private,结构体的默认访问说明符是public。
友元使用关键词friend。友元函数可以访问类的私有成员,友元类中的函数全部都是友元函数。友元类一般作为传递消息的辅助类,若B是A的友元类,则一般A是B的子属性,用B来修改A。
C++面向对象模型
C++类对象中的成员变量和成员函数是分开存储的。C++类中的普通成员函数都隐式包含一个指向当前对象的this常指针。
const修饰成员函数,表示*this不能被修改。 此时this不仅是常指针,更是常量常指针。
1 | double getReal() const; |
如果一个类中有同名的常量和非常量函数,算是重载。常对象和非常对象可以分别调用。
对象的构造与析构
构造函数是与类名相同的特殊成员函数,没有任何返回类型的声明。
当栈中的对象超出作用域时,对象会被销毁,这时会发生两件事,调用对象的析构函数并释放对象的内存。先被创建的对象后释放。
无参构造函数(默认构造函数)
用类创建对象时,调用无参构造函数,无参构造函数的调用不能加空括号,否则编译器会将其视为函数声明。
1 | Complex c1; //调用默认构造函数 |
如果没有显式地声明构造函数,则编译器会提供默认的无参构造函数。
default和delete
如果希望C++保留默认构造函数,可以使用default。如果不希望使用构造函数,可以使用delete。
1 | class MyClass1 { |
可以将拷贝构造函数和operator=设为delete来禁止赋值和按值传递。
有参构造函数
有参构造函数有以下调用形式。
1 | Complex c1(1.0, 2.0); |
拷贝构造函数
编译器生成的拷贝构造函数的具有默认形式:
1 | MyClass::MyClass(const MyClass& c) |
如果没有显式地声明拷贝构造函数,编译器会提供默认的拷贝构造函数。
C++传递函数参数的默认方式是值传递,实参初始化形参时使用拷贝构造函数。
1 | Complex c1 = old_c; //调用拷贝构造函数 |
拷贝构造和赋值运算符
如果函数返回匿名对象,给对象赋值则会使匿名对象析构,如果使用匿名对象初始化一对象,则匿名对象会转化为新的对象。
1 | Complex fun() { |
总之,声明会使用拷贝构造函数,而赋值语句会使用赋值运算符。
移动语义
应当实现移动构造函数和移动赋值运算符,它们可以将右值的所有权交给现在的变量。只有知道源对象即将销毁时移动语义才有用。移动结束之后需要将源对象设为nullptr以防源对象的析构函数释放这块内存。
1 | class Sheet { |
标准库的swap()也是依赖移动语义实现的,避免了所有复制。
1 | template <class _Ty, class> |
五规则和零规则
如果类中动态分配了内存,通常应当事先析构函数、拷贝构造函数、移动构造函数、赋值运算符与移动赋值运算符。
但在现代C++中,应当避免旧式的、动态分配的内存,而改用现代结构。
构造函数初始化器
可以使用构造函数初始化器初始化成员。有参构造成员、const和引用必须在初始化器中赋值。 如果初始化器有多个成员,按照成员的定义顺序构造它们。
1 | MyClass::MyClass(Complex c) |
委托构造
委托构造允许构造函数调用该类的其他构造函数,这个调用必须使用构造函数初始化器,且必须是唯一的成员初始化器。
1 | Complex::Complex() |
运算符重载
运算符函数是一种特殊的成员函数或友元函数。成员函数具有this指针,而友元函数没有this指针。
二元算数运算符一般重载为全局函数,因为有时需要隐式的类型转换或自定义类型在运算符右边的情形。
1 | friend Complex operator+(const Complex& c1, const Complex& c2){ |
前置++和后置++用一个int占位参数进行区分。前置++返回引用,后置++返回值。
1 | //前置++ |
不要重载&&和||,这会让它们的短路功能失效。
为了满足链式编程的需求,重载<<和>>时需要返回流的引用。
1 | friend std::ostream& operator<<(std::ostream& out, const Complex& c) { |
对于一些容器,往往需要重载下标运算符。为了提供只读访问,往往还提供const版本。下标运算符并不一定只能接受整数,也可以接受其他类型作为键。
1 | template <typename T> |
下标运算符不能接收多个参数,可以重载函数调用运算符。此外,重载函数调用运算符可以将对象伪装成函数指针,然后将函数对象当成回调函数传给其他函数。
为实现类型转换,需要重载类型转换运算符。类型转换运算符函数不需要返回值类型,因为运算符名即确定返回类型。
1 | Complex::operator double() const |
不过此时会出现多义性,可以用将构造函数或类型转换运算符函数标为explicit来禁用自动类型转换。
1 | Complex c(1.0, 2.0); |
将double()标为explicit后,下面可以使用:
1 | Complex c(1.0, 2.0); |
代码重用
继承与派生
继承模型
子类继承父类的全部成员变量和除了构造及析构以外的成员函数。
类型兼容性原则:子类就是特殊的父类。子类是父类成员叠加子类新成员得到的。
- 子类对象可以当做父类对象使用
- 子类对象可以直接赋值给父类对象
- 子类对象可以初始化父类对象
- 父类指针可以直接指向子类对象
- 父类引用可以直接引用子类对象
当子类和父类有同名成员时:
- 子类的成员屏蔽父类的同名成员
- 访问父类同名成员需要使用父类的类名限定符
- 父类成员的作用域延伸到所有子类
构造和析构
子类对象在创建时:
- 首先调用父类的构造函数
- 父类构造函数执行结束后,执行子类的构造函数。
- 当父类的构造函数有参数时,需要在子类的初始化列表中显式调用
- 析构函数的调用顺序与构造函数相反
当继承和组合混搭时:
- 先构造父类,再构造成员变量,最后构造自己
- 先析构自己,再析构成员变量,最后析构父类
多态
重载、重写、重定义
重载:
- 必须在同一个类中进行
- 子类无法重载父类的函数,父类同名函数会被覆盖
- 重载发生在编译期间,根据参数表列决定函数调用
重写: - 必须发生在父类和子类之间
- 父类和子类必须有相同的函数原型
- 使用virtual声明
- 多态在运行期间根据具体类型决定函数调用
重定义: - 不使用virtual,子类覆盖父类函数
将所有方法都设为virtual,除了构造函数(因为实例化子类对象时必须逐个调用父类的构造函数)。尤其是析构函数,这可以防止意外地忽略析构函数的调用。
如果需要在子类中重写某一方法,始终使用override关键字,确保重写的正常进行,而不是意外地创建了新的虚方法。
1 | class Base |
不能重写静态方法,因为静态方法属于类而不属于对象。
应当重写重载方法的所有版本,可以显式地重写也可以使用using关键字。
1 | class Derived :public Base { |
多态的实现
- 子类继承父类
- 子类重写父类虚函数
- 父类指针/引用指向子类对象
不要到数组使用多态,因为指针步长不一定相等。
多态原理
当类中声明虚函数时,编译器会在类中生成虚函数表。虚函数表是一个存储类成员函数指针的数据结构,由编译器自动生成和维护,virtual成员函数会被编译器放入虚函数表中。
当存在虚函数表时,每个对象都有一个指向虚函数表的VPTR指针,VPTR一般作为类对象的第一个成员。
编译器不区分对象是子类对象还是父类对象,它只区分是否是虚函数,是在虚函数表中寻找函数的入口地址
初始化子类的vptr时,vptr会分步依次指向父类的虚函数表。
多继承
多继承有可能会带来二义性,尤其是多继承了同属于一个父类的两个子类。虚继承可以保证被继承的父类只会构造一次。
解决“菱形”问题的最好方式是将最顶层的类设为抽象类,将所有方法设为纯虚方法,只声明方法而不提供定义。如果顶层的类提供了方法的实现,则可以虚继承这个类,子类将视虚基类没有任何方法的实现。
多继承的合理应用是混入类,这种混入类通常以-able结尾。
article_txt