C++记录

Posted by xnchen on May 26, 2021

[TOC]

摘要

课程链接

基础

基本数据类型

整型

image-20210603162823013

浮点数

image-20210603162848910

其他数据类型

image-20210603162952099

枚举类型

enum Fruit {
    APPLE=1,
    BANANA=2,
    ORANGE=3,
};
Fruit f=APPLE;
// 默认4字节大小,也可以指定枚举类型:冒号加类型名
enum Fruit : long long {
    APPLE=0x00000000000000001,
    BANANA=2,
    ORANGE=3
};

Union联合体

所有成员共享同一块内存

指针和引用

指针
  • 指针是一个变量,值是另一个变量的地址,地址在32位/64位系统上是4字节/8字节。
  • 使用星号(*)访问指针所在内存地址中存的值
int var=20;
int *ip=&var;
引用
  • 引用是已存在变量的另一个名字,指向同一块地址,如果一个发生变化,另一个也一定发生变化
  • 和指针的区别:一定不存在空引用;引用无法改变指向对象,必须在被创建时初始化
int var=20;
int &r=var;

特殊关键字

const

常量是固定值,在程序执行期间不会改变。

常量和指针:一个好的辨认方法是将*读作”pointer to”,然后对关键字从右往左读。

int *const p; 	// 指针是常量,指向的值可以发生变化(const pointer to int)
const int p;	// p是常量,无法发生改变
const int *p;	// p的值(指针)可以发生改变,指向的值不能发生改变(pointer to const int)
const int *const p;	// 指针和值都不能发生改变(const pointer to const int)

constexpr

  • 告诉编译器这是编译时常量,因此可以使用这个关键字做编译时优化,运行时性能比const好
  • 限制:
    • 不能包含函数调用或者对象构造
    • 返回值不能是void类型,不能声明新变量或新类型
constexpr int a=3;

constexpr int get_five() {return 5;}
int some_value[get_five()+7]; // 本来在数组长度中使用函数是不可以的,但是因为constexpr存在,在编译时已经是常量了

typedef

类型别名,用来为一个已有的类型取一个新的名字,在复杂项目中提高代码可读性

typedef int* GLAPIP;
int a=3;
GLAPIP b=&a; // 即等同于 int* b = &a;

volatile

告诉编译器这是一个易变量,每次从内存地址读取这个数据。

int i=10;
int a=i;
// 这其中用编译器不知道的方法修改了i=32
int b=i;
// 结果b还是10

// 使用volatile
volatile int i=10;
int a=i;
// 使用编译器不知道的方法修改i=32
int b=i;
// b是32,a是10

其他运算符

运算符 作用 例子
sizeof 返回变量的占位大小,单位是byte int a; sizeof(a)的值为4
condition?X:Y 三元运算符  
. 引用结构体对象的成员 S.a,返回结构体S的成员a
-> 引用结构体指针对象的成员 S->a
& 获取变量的地址  
* 得到对应地址的变量  

逻辑控制语句

  • 条件判断
  • 循环
  • 跳转语句

if-else

if (表达式) {
    ...
} else {
    ...
}

switch

swicth (表达式){
    case constant:
    	...
    	break;//可选
    case constant2:
    	...
        break;
    default:
    	...
}

for

for (init; condition;increment){
    ...
}
vector<vector<int>> dim2Array;
for (vector<vector<int>>::iterator iter=dim2Array.begin(); iter!=dim2Array.end(); iter++){
    for(vector<int>::iterator iter2=iter->begin(); iter2!=iter->end(); iter2++){
        std::cout << *iter2 << std::endl;
    }
}
// 使用auto进行简单化
for (auto& iter=dim2Array.begin(); iter!=dim2Array.end();iter++){
    for (auto& iter2=iter->begin(); iter2!=iter->end(); iter2++){
        std::cout << *iter2 << std::endl;
    }
}
// 使用for的语法简单化
for (auto& iter : dim2Array){	// 引用一定要加,不加的话会一直构造一维数组
    for(auto& iter2 : iter) {
        std::cout << iter2 << std::endl;
    }
}

while

while (表达式){
    ...
}

do while

do {
    ...
} while (表达式)

break

跳出当前循环(不会连着跳两个范围)

continue

放弃本次循环后面的语句,但是不会跳出循环。

goto

直接跳转到被标记的语句

goto finish;

finish:
...

注:因为goto太灵活,打乱代码执行顺序,导致代码可读性、可维护性差,不推荐使用

函数

函数的声明

return_type function_name(parameter list);

int max(int num1, int num2);

函数的定义

return_type function_name(parameter list){
    ...
}

int max(int num1, int num2){
    int result;
    result = num1>num2?num1:num2;
    return result;
}

函数参数类型

  • 传值

    void swap(int x, int y){
        ...
    }
    

    可以理解为将参数深拷贝

  • 传引用

    void swap(int &x, int &y){
        ...
    }
    

    实际上这个就是对x和y的操作

  • 传指针

    void swap(int *x, int *y){
        ...
    }
    
  • 传数组

    // 一维数组
    void func(const int*);
    void func(const int[]);
    void func(const int[10]);
      
    // 二维数组,比较特殊,因为C++中二维数组在内存中的存放方式是线性的,因此需要制定列长度
    void func2(const int a[][10]);
    void func2(const *a[10]);
    void func2(int **a);	// 指针的指针
    

默认参数

  • 在函数定义中设置
  • 在函数声明中设置

注意:

  • 默认参数一定要设置在函数被调用之前。当函数声明已经设置了默认参数,就不允许在函数定义中再次设置。
  • 默认参数的右边不允许出现非默认参数。

函数修饰符

extern

在修饰函数或者变量时,告知编译器该函数或变量已经在别处定义,可以放心编译。

声明extern关键字的全局变量和函数可以使得它们能够跨文件被访问。

// 在test.header中指定
extern int i;
extern int test(int a, int b); // 函数在head文件中默认是extern的,所以这个修饰符可加可不加

// 在对应的同名cpp:test.cpp中定义变量和函数
int i=3;
int test(int a, int b){
    ...
}

// 然后在其他cpp文件中使用:
extern int i;
extern int test(int a, int b);
static

和extern相反,告知编译器函数只限制在本文件内调用,不能在别的文件被调用

static int test(int a, int b); // 声明静态函数

##### inline

inline的函数会在调用点展开,减少调用开销,提高代码执行效率。但可能会带来代码体积的膨胀(inline函数不要超过十行)。相比于宏定义来说,inline有类型检查,更安全。

在函数定义的前面加上

inline int test(int a, int b) {
    ...
}

函数重载

允许拥有相同名称的函数,但是参数列表和定义不同。

image-20210603210611274

可变参数

在无法给出所有传递给函数的参数的类型和数目时,可以使用省略号(…)指定函数参数表。

void fun1(int a, double b, ...);

为了解决变长参数问题,需要以下几步:

  1. 定义一个va_list类型的变量,变量是指向参数的指针。
  2. va_start初始化刚定义的变量,第二个参数是最后一个显式声明的参数。
  3. va_arg返回变长参数的值,第二个参数是该变长参数的类型。
  4. va_end将1定义的变量重置为NULL。
void foo(int a, ...) {
    va_list pArgs = NULL;
    va_start(pArgs, a);
    int dwVarArg = va_arg(pArgs, int);
    int dwVarArg = va_arg(pArgs, char);
    va_end(pArgs)
}

尾置返回类型

尾置返回类型是在C++11标准中新增的语法,可以用于任何函数定义中,旨在方便复杂函数的定义。

尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,需要在本应该出现返回类型的地方放置一个auto关键字。

// func接受了一个int类型的实参,返回了一个指针,该指针指向一个含有10个整数的数组
auto func(int i) -> int (*)[10];

返回指向函数的指针的函数

……C++,充满了人类不可达的语言奥妙

长这样:

bool (*pf)(const std::string, const std::string);// pf指向一个返回值是bool的函数指针
#include <iostream>
 using namespace std;
 
int funTwo(int a, int b)
{
	return a * b;
}
 
// funOne是一个函数,带有一个int型参数,它返回一个指向函数的指针
// 这个指向函数的指针指向一个返回int型,并带有两个int型的形参的函数
int (*funOne(int number))(int a, int b)
{
	cout<<number<<endl;
	return funTwo;
}
// 使用尾置返回类型,可以写成:
auto funcOne(int number) -> int (*)(int a, int b){
    ...
} 
 
int main()
{
    cout<<funOne(5)(3, 10)<<endl;
    return 0;
}

命名空间

用不同的命名空间对不同项目进行分隔。

把限制范围的代码使用namespace 命名空间名字 {}括起来。使用的时候就是命名空间名字::

namespace first_space {
    void func(){
        cout << "awsl"<<endl;
        int val1 = 1;
    }
}
int main(){
    first_space::func();
    int a = first_space::val1;
}

命名空间可以嵌套使用,使用的时候就name1::name2::以此类推

命名空间别名

namespace 新的名字 = 原来的名字;

using

using namespace std;

匿名命名空间

当命名空间的内容只想在本文件使用的时候,可以用匿名命名空间,编译器会给这个匿名命名空间唯一的名字,并且在本文件中使用using namespace。作用和static类似,但是更加简洁。

namespace {
    int i=0;
}
int main(){
    std::cout<<i<<std::endl; // 不需要添加name::
}

类与对象

访问控制

  • public
  • protected
  • private
    • 默认控制权限
  • friend
    • 友元函数or友元类,可以访问该类的private或者protected成员
    • 友元关系是单向的,只有被声明的那个类可以访问,而不可以访问自己声明的友元的私有成员。
    • 要避免友元,因为访问控制不合理。
class Teacher;
class Student {
public:
	void Setid(uint32_t id) {m_id=id;}
    uint32_t GetId();
    friend Teacher;
    friend void PrintStudentId(Student);
private:
    uint32_t m_id=0;
    uint32_t m_age=18;
}; // 类的定义大括号最后要加一个分号

uint32_t Student::GetId(){
    return m_id;
}

创造与删除类对象

// 创造
Student smith; 					// 访问类属性用smith.[成员]
Student *jony = new Student;	// 访问类属性用jony->[成员]
// 删除
delete jony;

构造函数

  • 构造函数和类同名。

  • 未定义构造函数时,编译器自动生成不带参数的默认版本
  • 执行构造函数鲜执行初始化列表,再执行函数体
class Student {
public:
    Student();
    Student(uint32_t id);
    Student(uint32_t id, uint32_t age);
private:
    uint32_t m_id=0;
    uint32_t m_age;
};

Student::Student(uint32_t id, uint32_t age)
    : m_id(id), m_age(age)	// 把构造函数的输入参数传递给类成员,初始化该类成员。注意这里的初始化顺序需要和类内定义顺序一样
{
	// 一些初始化语句      
}

Student::Student() : Student(0, 18)
{
    ...
    // 调用双参数初始化方法,委托初始化
    // 无参版本在定义的时候不要使用空括号,会被认为是函数声明
}

Student::Student(uint32_t id) : Student(id, 18){
    ...
}
  • 使用default关键字可以要求编译器生成默认构造函数:Student()=default;

  • 使用explicit关键字可以拒绝对象被隐式构造:

    void foo (Student stu) {
        //do something
    }
      
    int main(void){
        uint32_t id=4;
        foo(id); // 虽然传入是uint32_t,但是编译器会调用Student类的单参数构造函数,隐式构造新的对象
    }
    

    因此采用explicit Student(uint32_t id);

  • 使用delete关键字删除部分构造函数,避免对象不符合预期的被构造:Student(char id)=delete; // 防止输入char类型

复制构造

赋值构造函数是特殊的构造函数,形参为本类的对象引用。

目的是用已经存在的对象初始化同类新对象。

  • 如果没有定义复制构造函数,编译器则自动生成默认的按比特位进行复制的复制构造函数,即浅拷贝。
class Student {
public:
    ... // 其他构造函数
        
    // 复制构造函数
    Student(const Student& stud){
        m_id = stud.m_id;
        m_age = stud.m_age;
        std::cout << "Student(const Student& stud)\n";
    }
    // 不希望对象被复制构造,可以用delete关键字拒绝:
    // Student(const Student& stud) = delete;
private:
    uint32_t m_id=0;
    uint32_t m_age;
};
移动构造

移动构造函数用于将某个临时对象的资源移交给另一个正在创建的新对象。

事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。

右值:C++中的右值指的是临时值或者常量(保存着CPU寄存器中的值)。

int&& a=5; // 5会被直接存放在寄存器中
int b=10;
int&& c=b; // 错误!b在内存中有空间,所以是右值,右值不能赋值给左值
int&& d=b+5; // 正确,b+5的结果存放在寄存器中,没有在内存中分配空间,因此是右值

左值:保存在内存中的值。

移动构造&&表示C++中的右值。只有创建对象时传入的是右值才会执行移动构造函数。

class Test{
public:
    // 移动构造函数
    Test(Test&& obj) : m_ptr(obj.m_ptr) {obj.m_ptr=nullptr; /*销毁传入*/}
private:
    int *m_ptr;
};
Test Func1() {return Test();}
int main(void){
    Test obj2=Func1();
    return 0;
}

完美转发:如果移动构造函数的输入是左值,会造成无法转发的问题。

解决方法:

  • 使用std:move()方法强制转换成右值
  • std::forward保持传入本身的属性,左值就是左值,右值就是右值
  • static_cast<T&&>和std::forward方法一样

析构函数

class TimePerf{
public:
    TimePerf(const char *name); // 构造函数
    ~TimePerf();				// 析构函数
private:
    const char *m_name;
    int64_t m_startTime;
};

TimePerf::TimePerf(const char *name) 
    : m_name(name), m_startTime(GetCurTime())
{
}

TimePerf::~TimePerf(){
    std::cout << m_name << "consume time:"
        << GetCutTime() - m_startTime
        << std::endl;
}

静态成员与常量成员

static:静态成员
  • 类的静态成员变量被所有对象共享,具有静态生存期,且不占用某个具体对象的内存。
  • 静态成员变量只能在类外定义初始化
  • 静态成员函数只能访问静态成员变量,可以使用加作用域限定符方式访问也可以通过对象访问。
const:常量成员
  • 常量成员只能通过类内初始值或初始化列表完成初始化。
class Student {
public:
    static uint32_t GetGender() {return m_gender;} // 静态成员函数,只能访问静态成员变量
private:
    const uint32_t m_grade=3; // 常量成员,只能在类内初始化或者初始化列表初始化
    static uint32_t m_gender; // 静态成员变量,这里只是成员变量的声明,定义和初始化需要在类外
}
uint32_t Student::m_gender=0; // 静态成员变量的定义和初始化

int main(void){
    Student stud(3);
    // 两种访问静态成员的方法
    std::cout << Student::GetGender() << std::endl;
    std::cout << stud.GetGender() << std::endl;
}

this指针

指向__当前对象__本身,与对象首地址相同。

隐含于类的每一个非静态成员函数参数列表中。

class Student {
public:
    int GetX() {return this->m_x;}
    int GetY() {return m_y;}
private:
    int m_x;
    int m_y;
}

类的运算符重载

对已存在的对象的赋值操作若需要使用特别的控制,可通过重载赋值运算符实现

class Test{
public:
    Test& operator+(const Test& b){ // 重载+运算符,输入参数b是+的右边的输入,其他运算符包括+、-、*、/、->等
        Test test;
        test.length = this->length + b.length;
        test.height = this->height + b.height;
        return box;
    }
private:
    double length;
    double height;
}

int main(){
    Test test1, test2;
    ...// 赋值
    result = test1+test2; // 重载后的+运算符
    return 0;
}
  • 可以通过重载new/delete操作符定制对象的内存分配和释放过程:
class Test {
public:
    void *operator new(size_t size) {return std::malloc(size);}//new返回内存地址,入参是本类对象占用内存
    void operator delete(void* ptr) {return std::free(ptr);}// delete返回void,输入本类对象指针
}
  • 可以重载函数调用操作符:
    • 类对象可以伪装成函数指针,相比于函数来说的优势是,类对象持有数据。
class Less {
public:
    bool operator()(int value) {return value<threshold;}
private:
    int threshold;
};

void Filter(int *array, int num, Less fn){
    for (int i=0;i<num;i++){
        if(fn(array[i])) std::cout<<array[i]<<"\n"; // 当array中数小于fn设定的阈值,才会被打印出来
    }
}

int main(void){
    int array[5] = {1,-4, 10, 0, -1};
    Less less_than(1);
    Filter(array, 5, less_than);
    return 0;
}

仿函数

仿函数(Functor)又称为函数对象(Function Object)是一个能行使函数功能的类。仿函数的语法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载 operator() 运算符。因为调用仿函数,实际上就是通过类对象调用重载后的 operator() 运算符。

class Greator {
public:
    explicit Greator(int base): m_base(base) {}
    bool operator()(int value) const {return value>m_base;}
private:
    int m_base;
};
int main(){
    int base=10;
    Greator greator(base); //构造仿函数对象
    greator(20);// 调用仿函数对象
    return 0;
}

类的组合与前向声明

  • 类的成员可以是另一个类对象
  • 组合类的构造函数需要对对象成员初始化

  • 前向声明用于某个类在被声明之前被引用,但仅限引用,不可对该类对象进行操作

类的继承

class Base{ // 如果一个类不想被继承,用final关键字:class 类名 final {};
public:
    Base(int a):m_a(a){}
    int GetA() {return m_a;}
private:
    int m_a;
};

class Derived: public Base{ // class 类名: 访问关键字 父类名 {};
public:
    Derived(int a, int b): Base(a), m_b(b) {}
    int GetB {return m_b;}
private:
    int m_b;
};

类成员访问限制

  • 友元关系不能被继承(你父亲的朋友不是你的朋友)
  • 访问权限限制:

image-20210604183658162

类的继承

派生类的构造函数
  • 默认情况下派生类需要定义自己的构造函数,不会继承基类的构造函数。

  • 可以主动使用using关键字继承基类的构造函数,但是只能初始化基类成员,其新增成员可以使用类内初始值完成初始化。

    • using关键字也可以用于继承父类非private成员函数
    class Derived:public Base {
    public:
        using Base::Base; // 会让所有基类构造函数全部继承过来
    private:
        int m_c=4;
    };
    
  • 派生类构造函数的执行顺序为:

    1. 基类构造函数;
    2. 派生类初始化列表中其余项;
    3. 构造函数体
    clss Derived: public Base{
    public:
        Derived(int a, int b, int c) : Base(a, b), m_c(c) {}
        Derived(int c): m_c(c){} // 这里父类需要有无参构造函数,不然会报错
    private:
        int m_c=4;
    }
    
派生类的析构函数
  • 基类的析构函数不能被继承,若派生类需要,课自行定义析构函数
  • 派生类析构函数不需要显式地调用基类析构函数,编译器会隐式调用
  • 派生类对象西沟时,鲜执行派生类析构函数,再执行基类西沟函数
虚继承

为了解决多重继承中的公共基类成员的二义性问题,使用虚基类机制消除二义性

// 二义性:
class A {public: void f();};
class B {public: void f(); void g();};

class C: public A, public B {
    // 如果实例化C c1; 在c1.f()中就会存在二义性
    // 一个方法是在调用时指明是哪个父类的函数:c1.A::f();c1.B::f();
}
class Base1 {
    ...
};
class Base2: virtual public Base1 {
    ...
};
class Base3: virtual public Base2 {
    ...
};
class Derived: public Base2, public Base3{
public:
    Derived(int a, int b, int c, int d): Base1(a), Base2(b), Base3(c) {}
    // 派生类构造时不止要对直接基类进行初始化,也要负责对虚基类初始化
}

类的多态

多态:操作统一接口具有表现多种形态的能力,或者同样的数据交给不同类型的实体处理时导致不同的行为。

由于多态性,相同的程序能够处理多种类型的对象。统一接口,多种实现。

多态的实现:虚函数

在基类的函数前加入virtual关键字,程序会在运行时动态绑定函数地址。

virtual关键字标识成员函数为虚函数。

class Vehicle {
public:
    Vehicle(int speed): m_speed(speed) {}  // 构造函数
    virtual void Drive(int distance) {std::cout << "cant move\n";}// 虚函数:加入virtual关键字
private:
    int m_speed;
};
class Car : public Vehicle{
public:
    Car(int speed): Vehicle(spped) {} // 构造函数
    void Drive(int distance) {
        ...
    } // 派生类重写
}; 
  • 虚函数不允许是静态成员函数与构造函数。

    • 对象虚表指针赋值发生在当前类的构造函数初始化列表执行前,其基类构造函数执行后。(因此__无法在构造函数中调用虚函数__。
    • 又因为析构和构造的过程相反,也__不允许在析构中调用虚函数__。

    • 也不允许在父类的构造函数与析构函数内调用虚函数。禁止在父类的构造函数与
  • 派生类重写基类虚函数时,可以添加override关键字标识。(可加可不加,方便编译器检查)

    void Drive(int distance) override {
        ... 
    }// 派生类重写
    
  • 若基类函数不希望被重写,可以使用final关键词拒绝

    virtual void Drive(int distance) final {std::cout << "cant move\n";}
    
  • 基类的析构函数最好都声明为虚函数
    • 这样销毁资源的时候,会自动地调用派生类的析构函数,派生类的析构函数又会自动调用父类析构函数。
  • 虚函数的实现方法:
    • 若某个类声明了虚函数,则该类及其所有直接、间接派生类,都各有一张虚表。
    • 虚表中有当前类的各个虚函数入口地址,包括自身新增和继承而来的。
    • 有虚表的每个类对象有用一个指向当前类的虚表的指针。
    • 编译器遇到虚函数调用时,产生虚表指针+表偏移的指令来确定函数地址,以实现动态绑定。
      • 先找到虚函数表指针
      • 在虚函数表中,找到被调函数的偏移(虚表指针+表偏移),指向函数实现
      • 找到目标虚函数的具体地址

RTTI(运行时类型识别)

  • 使用dynamic_cast可以在继承体系中完成类型的双向转换,并运行时检查类型转换合法性。
  • 使用typeid运算符可以获取基类类型指针或引用所指对象的实际类型(派生类类型)
  • 这两种方法都需要继承树中存在虚函数
void Foo(Logger* logger){	// Logger是父类
    std::cout << typeid(*logger).name() << std::endl; // 输出实际派生类名称
    
    ConsoleLogger * cLogger = dynamic_cast<ConsoleLogger *>(logger); 
    // 尝试把Logger类指针转换为ConsoleLogger类(派生类
    if (cLogger != nullptr){
        cLogger->SetTextColor(1); // 如果成功,说明是ConsoleLogger类
    } else {
        FileLogger * fLogger = dynamic_cast<FileLogger *>(logger);
        // 尝试把Logger类指针转换为FileLogger类(派生类
        if (fLogger != nullptr) {
            fLogger->SetFileName("Huawei");
        }
    }
}
  • 使用RTTI会带来很大开销
  • 程序设计合理的时候,用户本身不需要关心对象具体类型是什么
  • (ps:C++11的时候加入的decltype好像也可以获得 数据类型?

抽象类

纯虚函数:基类无需或无法给出定义的虚函数可被声明为纯虚函数,具体实现留给派生类定义。

抽象类:拥有纯虚函数的类被称为抽象类。抽象类无法被实例化。

  • 析构函数不能为纯虚函数:因为子类析构的时候必须调用父类析构函数。
  • 抽象类用于代码抽象和设计的目的,以约束不同的具体实现类对外表现出相同的接口。
class GameRole {
public:
    // 析构函数
    virtual ~GameRole()=default;
    // 下面两个是纯虚函数的定义
    virtual void ShowApprearance()=0;
    virtual uint32_t GetHealthValue()=0;
}

异常机制

image-20210608164705939

异常流程

  1. 首先检查异常发生的位置是否在当前函数的某个try块内,如果在的话,执行第二步。如果不在,则沿着函数调用栈重复第一步。
  2. 找到对应的catch块,使用RTTI(运行时类型检查)查看这个异常类型是否和catch块中的捕获类型相同。若相同,则执行3 ,若不相同,重复执行1和2.
  3. 处理异常,执行栈回退,释放try块中的局部变量。

RTTI、不断的回溯,以及较多的cache miss值导致异常性能较低的主要原因,另外,异常还会导致代码膨胀,可读性降低。所以只在非常必要的场景中使用,应该尽量避免用C++的异常机制

异常可能会导致内存泄露

void testExcept2(){
    std::string str="123456";
    char c=str.at(100);//str[100]不行,不会明确抛出异常
    std::cout << c;
}
void testExcept(){
    testExcept2();
}
int main(){
    try{
        int *test = new int[30];
        testExcept();
        delete testt // 执行不到,内存泄露!!!
    }
    catch (std::exception e){
        std::cerr<<"数组越界";
    }
    return 0;
}

解决方法:RAII(资源获取即初始化),以对象管理资源。在异常处理的栈展开过程中,封装了资源的对象会被自动调用其析构函数释放资源。

int main(){
    try {
        ArrayOperation array; //调出异常会自动析构,并且释放资源
        array.InitArray();
        testExcept();
    }
    catch(std::exception e) {
        std::cerr<<"数组越界";
    }
	return 0;
}

类型转换

  1. 隐性类型转换

    • 数值类型转换:char、short到int,float到double,编译器自动进行类型提升
      • 坑:负数转化为无符号数,会采用二进制补码表示(不警告有符号和无符号之间的差别)
      • 坑:bool值的转换,false等价于0或者空指针,true等价于其他任何数值或者转换为1
      • 坑:浮点数转换为整数会采取截断(把后面位数直接截断,不四舍五入)
    • 指针类型转换规则
      • 空指针可以转换到任意类型指针。任意类型指针都可以转换到void*
      • 继承类指针可以转换到可访问的明确的积累指针,不改变const或者volatile属性
    • explicit关键字组织构造函数进行隐式转换
      • 声明为explicit的构造函数不能在隐式转换中使用
      • 只能用于修饰类构造函数
      • 只能作用于单个参数的构造函数
      • 不能被继承
  2. 显示类型转换

    使用方式 xxx_cast <new_type> (expression)

    • static_cast
      • 只会在编译时检查,但是没有运行时检查保证转换安全性。
      • 用于类层次结构中基类和子类之间指针或者引用的转换:进行上行转换(子类->父类)是安全的;进行下行转换(父类->子类)时,由于没有动态类型检查,所以是不安全的
    • dynamic_cast

      • 要去转换的类型newt_ype必须是一个指针或引用或“指向void*“的指针
      • 如果new_type是指针,则expression的类型必须是指针。如果new_type是引用,则expression为左值。如果转型失败会返回nullptr(目标类型是指针)或者抛出异常(目标类型为引用)
      • 使用RTTI进行类型安全检查,因此存在一定的效率损失(但是比较安全
    • const_cast

      • new_type必须是一个指针、一用或者指向对象类型成员的指针
      • const_cast用于去除对象的const或者volatile属性
    • reinterparet_cast

      • 不常见
      • 对指针类型进行重新解释,类似纯C风格的指针类型强转
      • 转换结果与编译平台息息相关,不具可移植性
    • 类型转换操作符

      • 类型转换操作符是一种特殊的类成员函数,定义将类类型值转变为其他类型值的转换。转换操作符在类定义体内生命,在保留字operator之后跟着转换的目标类型。可以重载类型转换运算符:

        T1::operator T2() [const]; // const这个是因为转换函数一般不改变被转换对象,所以转换操作符通常定义为const
        // T1是类类型,T2是目标转换类型
        
      • 转换函数必须是类成员函数
      • 不能指定返回类型(返回值是隐含的,与转换类型相同,即上面的T2)
      • 形参列表必须为空
      • 支持继承,可以为虚函数
      • 只要存在转换,编译器可以在隐式转换的时候自动调用
      template<class T> 
      class Transfer {
      public:
          Transfer(int arg): i(arg){}	// 构造函数
          operator T() const { // 类型转换操作符重载
              return i;
          } 
      private:
          int i;
      }
           
      int main(){
          Transfer<double> t(3);
               
          double d1=static_cast<double>(t); // 显式转换
          double d2=t; // 隐式转换
               
          return 0;
      }
      

模板编程

函数模板

template <class T1, class T2, ...>
返回类型 函数名(参数列表)
{
    函数体
}

///使用例子:
template<typename T>
int compare(const T& a, const T& b){
    return a<b?-1:1;
}
int main{
    int a=1;
    int b=2;
    int result=compare(a, b);
    // or
    int result=compare<int>(a,b);
    return 0;
}
  • class可以用typename代替

  • 函数参数中,通过传递引用的方式效率更高

类模板

 template <class T1, class T2,...>
 class 类名 {
     ...
 }

// 例子
template<typename T>
class Operator {
public:
    int compare(const T& a, const T& b) {...}
};
int main{
    Operator<int> cmp_int;
    Operator<char> cmp_char;
}

成员函数模板

class Operator {
public:
    template<typename T>
    int compare(const T& a, const T& b){
        ...
    }
};

可变参函数模板

// 使用可变参函数模板实现print的方法
// 递归函数方式展开参数包,获取参数包中的每个参数
// 需要为递归栈开的参数包配置一个终止函数
template<typename Head, typename... Args>
void print(const Head& t, const Args&... args){
    std::cout<<t<<" ";
    return print(args);
}
// 递归终止函数
void print(){}
// 使用泛型编程,自动生成hook函数
Template<class F>
class Executor; // 前置声明

template<typename R, typename... Args>
class Executor<R(Args...)> {
private:
    using FunctionType=R(*)(Args...);// 这是一个函数类型?
public:
    Excutor(FunctionType func): func_(func){} // 构造函数
public:
    R execute(Args ... args) {
        // 加入的hook函数,记录该函数调用的次数
        ++count_;
        return func_(args);// 调用原本的函数
    }
private:
    FunctionType const func_;
    int count_ {0};
};

// read函数原型
ssize_t read(int fd, void* const buf, size_t count);

// 调用方式
auto executor1=new Executor<decltype(read)>(read); // decltype:可以得到函数类型
executor1.execute(fd, buf, count);

匿名函数

  • lambda表达式:可以携带数据,然后延迟调用
[捕获列表](采纳数列表) mutable(可选) 异常属性 -> 返回类型{
    //函数体
}
int value=1;
// 值捕获
auto copy_value=[value]{return value;};
// 引用捕获
auto copy_vaue=[&value]{return value;};
  • 基本语法:
    • 值捕获,再一次引用该函数的时候,如果值发生修改,返回是不会变的;引用捕获则会发生改变
    • []空捕获列表
    • [name1, &name2]按值捕获name1,按引用捕获name2
    • [this]捕获当前对象,使表达式可以访问该对象所有成员
    • [&]引用捕获所有变量,让编译器自行推导捕获列表
    • [=]值捕获所有变量,让编译器自行推导引用列表
    • [=, &x]默认按值捕获所有变量,除了变量x按照引用捕获
    • [&, x]默认按引用捕获所有变量,除了变量x按值捕获
    • [name=expr1, name=exp2,...]捕获一系列表达式
  • mutable作用:

    • lambda表达式默认为const函数,表达式内无法修改任何变量
    • 指定表达式为mutable属性,可以修改表达式为非const函数
    double data=1.23;
    auto capturingLambda = [data]() mutable {
        data *= 2;
        std::cout << "data= " << data << std::endl;
    };
    
  • 泛型lambda(C++14)

    • 参数标记为auto类型,编译器自动推导,推导规则与模板参数推导相同
    auto isGreaterThan10 = [](auto i) {return i>10;};
    int a=3;
    double b=35.2;
      
    isGreaterThan10(a);
    isGreaterThan10(b);
    
  • lambda表达式捕获列表不为空时,本质上是一个与仿函数类似的闭包类型
  • lambda表达式捕获列表为空时,可以转换为函数指针进行传递

算法示意

// 计算数组中大于阈值的数有几个
int main(void){
    std::vector<int> vec={1,2,3,4,5,6,7,8,9};
    int value=3;
    int cntlambdaCalled=0;
    // std::count_if(首位输入迭代器,末尾输入迭代器,条件(只带一个参数,返回True/False))
    // 用于返回区间中指定条件的元素数目
    int cnt=std::count_if(std::cbegin(vec), std::cend(vec), 
                          [value, &cntlambdaCalled](int i){
                              ++cntlambdaCalled;
                              return i>value;
                          });
    std::cout << cnt << std::endl;
    return 0;
}
// 排序
int main(void){
    std::vector<int> vec={9,4,6,2,7};
    // std::sort排序函数,采用类似快排的方法,时间复杂度nlog2(n)
    // std::sort(首位输入迭代器,末尾输入迭代器,排序方法(可以不写,默认从小到大))
    std::sort(vec.begin(), vec.end(), 
              [](int a, int b){return a>b;}
             );
    return 0;
}

初始化列表

  • 如果函数参数个数位置,但是类型相同,可以使用初始化列表类型作为函数形参
  • 初始化列表中的元素永远是常量值
void ErrorMsg(std::initilizer_list<std::string> ls){// 一个所有类型都为string的初始化列表
    for(auto beg=ls.begin();beg!=ls.end();beg++){
        std::cout << *beg << "";
    }
} 
int main(void){
    ErrorMsg({"functionX", "expected", "actual"});
    ErrorMsg({"functionX", exprcted});
    return 0;
}

类型推导

auto

  • 让编译器在编译期间自动推导变量的类型。
  • auto变量必须马上初始化
auto n=10;
auto p=&n;

decltype

  • 也是编译时推导,但是是以一个普通表达式作为参数,返回该表达式的类型。更加灵活,用起来稍微繁琐
int i=4;
decltype(i) a; // 推导i的类型,然后把a赋为该类型

using size_t=delcltype(sizeof(0)); //和using一起定义一个新的类型

// 下面定义一个匿名的结构体,然后结构体重用
struct {int d; double b;} annon_s;
decltype(annon_s) as;

// 结合泛型,获得函数返回值类型
template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y) -> decltype(x*y) {return x*y;}

智能指针

智能指针负责自动释放所知真的对象。C++定义了两种智能指针

  • unique_ptr:独占所指向的对象
  • shared_ptr:允许多个指针指向同一个对象

以及一个weak_ptr伴随类 指向shared_ptr所管理的对象

  • 需要独享某个资源时使用unique_ptr
  • 需要共享某个资源时使用shared_ptr
  • 优先使用make_unique和make_shared来初始化智能指针
  • 使用weak_ptr有两个用途
    • 防止shared_ptr出现缓
    • 获得一个可能随时被删除对象的临时使用权

image-20210609162826237

unique_ptr

  • 某个时刻只能有一个unique_ptr指向一个给定的对象
  • 当unique_ptr被销毁时,它所指向的对象也被销毁

image-20210609163131063

shared_ptr

引入记数的方式管理资源,如果资源的计数值为0,则释放资源

image-20210609163451866

weak_ptr

不控制所指向对象生存期的智能指针

  • 指向一个由shared_ptr管理的对象
  • 不会改变绑定的shared_prt的引用计数
  • 即使有shared_ptr指向,对象还是会被释放

image-20210609163736883

STL容器

STL容器都提供的操作:

  • begin:生成指向容器中第一个元素的迭代器
  • end:生成指向容器中伪元素之后的元素的迭代器(区间描述:[begin, end)
  • 类型别名
    • iterator / const_ietrator:容器类型的迭代器类型,const是只读版本
    • size_type:无符号整数类型,足够保存此种容器类型最大可能的容器大小
    • value_type:元素类型
    • reference:元素左值类型,与value_type&含义相同
    • const_reference元素的const左值类型(即const value_type&)
  • 大小
    • size():容器中元素的数目
    • max_size():容器中可以保存的最大元素数目
    • empty():判断容器是否为空
  • 对容器c添加、删除元素args(不适用array)
    • c.insert(args):将args中的元素拷贝进c
    • c.emplace(inits):使用inits构造c中一个元素
    • c.erase(args):删除args指定的元素
    • c.clear():删除c中所有元素
  • 关系运算符
    • ==、!= 所有容器都支持
    • <、<=、>、>= 关系运算符(无需关联容器不支持)

顺序容器

image-20210609165425808

  • 向顺序容器(非array)添加元素的操作:

image-20210609165502190

  • 向顺序容器(非array)删除元素的操作:

image-20210609165553051

  • 访问顺序容器

image-20210609165615996

容器适配器

  • stack:
    • 核心接口:push/pop/top
  • queue:
    • push/front/back/pop
  • priority_queue(优先队列)
    • 默认使用operator<形成降序排列
    • 核心接口:push/pop/top

默认情况下,stack和queue都是使用deque实现,priority使用vector实现

关联容器

image-20210609170339588

  • 向关联容器添加元素

image-20210609170404349

  • 向关联容器删除元素

image-20210609170428462

  • 访问关联容器

image-20210609170501045

无序容器

  • 不用比较运算符组织元素,而是使用哈希函数和==运算符
  • 因为不需要维护顺序,性能会更好
  • 可以自定义hash函数与==运算符

image-20210609170650726