0.windows gcc

1.基础概念

1.编译过程
  1. 预处理:去除注释、头文件展开
  2. 编译: 生成特定的汇编文件
  3. 汇编:生成二进制文件
  4. 链接:多二进制文件连接成可执行文件
2.c++在哪里查找函数,查找顺序:
  1. ->如果声明说函数是静态的则只本文件中查找,没找到报错
  2. ->没有声明是静态的则在所有程序文件中查找,找到两个以上报错
  3. ->库文件中查找,有些编译器-连接程序需要显示的指出收索哪些库,编译加-l
3.c++相对于c
  1. 封装: 隐藏内部实现,面向接口编程,这样对实现的调整无需修改使用端的代码
  2. 继承: 代码复用 有has-A is-A
  3. 多态: 见虚函数
4.大小
  1. bit->Bit->KB->MB->TB
  2. bit->char->short->int->long->longlong
  3. float->double->long double
5.数字存储
  1. 负数的模型,是通过数值最高位的越界实现的
  2. 浮点数的存储模型是有效位+指数位

2.语法基础

1.需要理解一个指针所指向的区域
char arr[] = "hello" //1.arr指向栈 这里可以理解为拷贝构造
char *p = "hello"    //2.p指向字符常量区
                     //3."hello"代表的是字符串在内存中的地址,在字符常量区为只读
2.数组名
  1. 数组名是常量,所以将字符串放入字符数组中(可以使用初始化)不能用赋值,得用strcpy,strncpy
  2. 数组名是数组首元素的地址,且步长为数组单元素的长度
  3. 如果想步长是整个数组的长度则需要对数组名在&一下
  4. sizeof数组名得到的是数组长度,sizeof指针得到的是指针大小通常为4个字节,所以数组名作实参时需要传入数组的长度
3.赋值/初始化 声明/定义/调用
  1. 变量的赋值,需要两个类型一致,或者可以隐式转换,或者对象重载了=、强转运算符
  2. 变量的初始化,不需要两个类型一致。和对象构造对应
  3. 变量的定义/声明 有无extern
  4. 函数的定义/声明/调用 (6.1.3)
4.static
  1. 定义全局变量,则变量只能在该文件中使用
  2. 定义局部变量,则声明周期是整个程序
  3. 静态变量的数目在程序运行期间时不变的所以编译器分配固定的内存来存储这些变量
5.宏和typedef
#define INT_P int *
INT_P pa, pb;       //声明了一个指针pa和一个int变量pb
typedef int * INT_P //语法:使用typedef的复杂类型只需要将别名替换在变量名的位置就可以了
6.分支语句
  1. switch case,程序不会在执行到case处以后自动停止,而是遇到break后停止
  2. if elseif else可以看成if else if else两个分支语句的嵌套 (所以只会有一个分支线被执行)
7.i++/++i
++i //将值+1,然后返回结果
i++ //先复制一个i的副本,然后i值+1,返回i的副本 所以i++的效率更低
8.const/常量
int ②* ③p   //const加在①②是修饰*(即数据),加在③是修饰p(即指针)
9.数组指针/指针数组
int *p[N]    //[]的优先级更高,所以是数组,数组中的元素是指针
int (*p)[N]  //*和p先绑定了,所以是指针,指针指向一个元素为N个的int数组 N不同p的类型也不同
             //这个类型和二维数组的数组名是同一个类型,所以该指针能指向对应的数组的首地址
int p[M][N]
int **p
//可以画出这2^2种类型的2维结构图,对应的还有2^3种的三维结构图(例如int ***p),带有*的那一个轴可以是射线 []的轴是线段
10.delete
int *p = new int;          //在堆上new一个int 对应delete p;
int *p = new int[1024];    //在堆上new一个数组 对应delete [] p;
char buf[1024];
int *p = new(buf) int[64]; //在数组buf(栈空间)上new一个数组,相当于内存池不需要delete []
                           //最后一种方法如果new的是对象,则需要显示的调用析构函数,防止对象中的指针内存泄漏
11.杂
  1. 数字如果使用特殊的后缀可以指定特定的存储类型
  2. c++中的每个表达式都有值
    int a=b=c=10; //so这个是合法的 c=10的返回值是10 即a=10
  3. 逗号运算符是一个顺序点,即先计算第一个表达式,然后计算第二个表达式
    ||、&&也是顺序点
  4. 头文件能包含的内容,原则是在头文件展开时不会产生二义性
    函数声明/#define或const定义的常量(内联的)/结构体声明/类声明/模板声明/内联函数(编译时展开)
  5. 命名空间,可以分批加入,可以在一个命名空间解开另外一个命名空间

3.函数

  1. 实参和形参之间需要严格的类型一致,或者可以隐式转换
  2. 使用内联函数的原因: 会有类型检测,可以避免宏没有带括号引起的逻辑错误
  3. 函数在传递指针和数组名时,实参和形参之间是指针的值拷贝,而不是指针所指向的数据拷贝
  4. pf是函数指针,pf()和(pf)()都可以实现函数调用,那么pf和(pf)是等价的?
    这里是一个折衷,容忍逻辑上无法自圆其说正是人类思维活动的特点(243页)
  5. 为什么需要函数声明,因为c++是分文件编译的,先告诉编译器这个函数是存在的,然后等待链接
  6. 参数使用const的原因
    a.避免误修改
    b.使用const参数的函数能接受const和非const的实参否则只能接受非const的
    c.函数参数是引用,当传入的参数类型不匹配或是一个表达式(即没有内存) 1.引用不是const则报错) 2.是const则会生成一个临时变量让其内存被函数参数所引用则不会报错。所以参数是const引用会使得函数更通用
  7. 左值表达式必须标识一个可修改的内存块
  8. 通过后置类型、decltype推导类型、auto 解决不确定类型的问题
    template<class T1, class T2>
    auto fun(T1 x, T2 y) -> decltype(x+y)
    {...; return x+y;}

4.指针

1.指针的两要素
  1. 指针:首地址(即指针指向哪里)
  2. 步长
2.野指针
//定义: 在指针创建时会分配指针的内存,但是不会分配指针所指向数据的内存
int *p; *p=10; //这里的p就是野指针
3.智能指针
3.1. 解决的问题&原理

1.所解决的问题: 在函数碰到异常退出时,函数已经分配的堆内存泄漏
2.原理: 智能指针是行为类似于指针的类对象,当栈中的对象消失时,会主动调用析构函数,在堆里面的内存可以在析构函数中释放掉

3.2. 处理浅拷贝

1.定义:在赋值或拷贝构造以后,多个指针对象指向同一块内存,会调用多次析构函数导致程序崩溃
2.unique_ptr、auto_ptr建立所有权,让赋值和拷贝构造操作转让所有权,unique_ptr策略更严格
3.share_ptr,采用应用计数,当计数从1到0的时候才调用delete

3.3. auto_ptr和unique_ptr的区别

1.auto_ptr在源智能指针和副本智能指针同时存在的时候编译可以通过,但是运行后行为不确定
2.unique_ptr会不让编译通过

unique_ptr<string> pu1,pu2;
pu1 = unique_ptr<string>(new string "hello"); //allowed 源智能指针只活了一行
pu2 = pu1; // not allowed

3.另外unique_ptr支持new[]版本

unique_ptr<double []>pda(new double[5]);  //在析构函数会调用delete[]
3.4. weak_ptr 是为了解决share_ptr的循环引用而引出来的,不会增加引用计数,只能通过lock返回share_ptr访问指向内容. 循环引用是指

1.构建两个对象A和B
2.在用两个share_ptr指针 ptr_a指向对象A,ptr_b指向对象B
3.最后A中有一个m_ptr_b让他指向B,B中有一个m_ptr_a让他指向A
4.在程序结束以后ptr_a和ptr_b在栈上被回收,但是对象A和对象B都在堆中切引用计数不为0,不会自动调用析构函数
5.解决这个问题是m_ptr_a或者m_ptr_b被定义成weak_ptr不会增加引用计数在ptr_a和ptr_b被回收以后B或A的引用计数为0就会被回收,在类内的智能指针也会被析构,另外一个类的计数也变成0然后被回收

5.引用

1.语法
int tmp;
int & r = tmp;
2.理解
  1. 在定义的时候需要被初始化,可以直接理解为变量或内存的别名
  2. 也可以理解位常量指针在初始化(即构造函数),左边常量指针,右边变量名
  3. 函数参数为引用时,可以理解为在值传递的时候给内存起了个别名

6.OOP

1.杂
  1. 类和结构体唯一的区别是类中默认是private,结构体中是public
  2. 避免构造函数中的赋值混乱一般在数据成员名中使用m_前缀
  3. (套用一句经典的话:编译器将可以解释为声明的语句一律解释为声明)
    class A;
    A x;    //定义一个对象x,调用默认构造
    A y();  //声明一个y函数,返回值为类A的对象
  4. 关于只有一个析构函数,所以所有的构造函数对同一变量都必须使用相同的方法new内存,要不用new要不用new[ ]不能混用,或者设置为空指针
2.什么时候调用析构函数
  1. 创建的是静态存储的对象,则在程序结束是自动调用
  2. 自动存储的对象在其生命周期消失的时候自动调用
  3. 在堆中创建的对象,调用delete时调用
  4. 在内存池中的对象需要自己调用
3.空类
  1. 一个空类编译器会实现的函数 构造/析构/拷贝构造/赋值运算符/地址运算符
  2. 拷贝构造: 包括定义和初始化,有些编译器会生成一个临时对象来构造对象
  3. 赋值运算符: 当前语句中只有赋值,没有定义
4.拷贝构造函数
  1. 什么时候会调用
    每当程序生成了对象副本时,编译器就会调用拷贝构造(433页)
    例如:用一个对象来初始化另一个对象/值传递/函数返回对象
  2. 关于重载赋值运算符重载需要注意的事项 (437页)
    1.防止自己赋值给自己
    2.防止浅拷贝
    3.防止内存泄漏
    4.返回当前对象的引用解决连续赋值
5.关于为什么需要友元
  1. 对于运算符重载需要表达式中先出现的是对象才能被调用,如果先出现的是非对象则需要友元
  2. 对于一些类已经写好了,但是需要重载他和一些新类型的数据的运算符,重写类并不是一个好方法,所以需要友元
  3. 只有在类的声明中才能声明友元函数,可控制哪些函数才能访问对象的私有数据,所以一定程度上来说友元并没有破坏面oop的封装
6.隐式转换
  1. 隐式转换,存在于初始化/赋值/实参到形参
  2. 通用类型到用户类型的隐式转换,需要用户类型有一个单参数的构造函数,加explicit拒绝隐式转换
  3. 用户类型到通用类型的隐式转换,需要重载强转运算符,如 operator int() const;
7.static
  1. 类中的静态变量,不属于某个对象,属于整个类,用来统计类相关的数据
  2. 声明在类内.h文件,初始化在类外.cpp文件,如果在.h文件中初始化会导致包含该.h文件就会产生一个副本导致错误
  3. 静态成员函数只能使用类中的静态变量,不能使用this指针
8.初始化列表
  1. 初始化列表: 在构造函数中必须用这种方式来初始化非静态的const数据成员和引用(c++11之前 c++11可以在类的声明中直接初始化)
  2. 数据初始化的顺序与他们在类声明中的先后出现顺序相同,包括继承。
  3. 语法: 构造父类使用类名来调用父类的构造方法,对于成员对象使用成员名
9.类内/类外
  1. 类内使用是说类定义的函数里面,类外是指通过对象直接点出类成员
  2. public:在类内和类外都能使用 protected:类内和子类内使用 private:类内使用
10.is-a/has-a
  1. B是A的子类,B中包含A类的对象. 统一称为B为包含类,A为被包含类
  2. 接口:类的函数声明 实现:类的函数定义
  3. 获得实现: 在B类里面可以调用A类的接口
  4. 不获得接口: 是指在B类的声明里面没有A类的函数,即在B类外不能直接调用到A类的接口(需要再次封装才能对外暴露)
  5. is-a 公有继承,子类继承了父类的实现和接口(不包括父类的私有接口)
  6. has-a 组合和保护继承(或私有继承),只获得实现不获得接口 (保护继承会获得接口的但不对外暴露)
  7. has-a 在B类内需要使用A类的protected成员的时候使用继承,否则使用组合
  8. protected继承/private继承获得的实现是一样的,protected继承会把A类的public/protected接口继承下来变成protected,private继承不继承A类的接口
11.虚函数
1.多态
  1. 虚函数:继承时的子类虚函数会替换子类虚继表中的父类的虚函数从而可以动态选择调用函数
  2. 非虚函数,程序会直接根据引用或指针的类型来选择要调用的方法(静态编联)
  3. 多态:父类指针(引用)指向之类对象,调用虚函数时是子类的虚函数被调用(动态编联)
  4. 虚函数的成本:每个对象都有一个虚函数表指针(标识这个对象实际归属于哪一个子类),每个类都有一个虚函数表(指针数组),每次虚函数调用时就会通过虚函数表指针找到对应类的虚函数表调用对应的虚函数,动态编联
2.虚析构函数/虚构造函数
  1. 虚析构函数: 保证了正确的析构函数序列被调用,如果不是虚的就只有指针或引用对应类型类的析构函数被调用 (通常是父类指针指向子类对象 所以父类的析构函数会被调用,子类的虚构函数不会被调用)
  2. 虚构造函数: 构造函数是发生在对象初始化的时候 虚函数发生在对数据改变的时候的多态行为,是属于两种概念,所以并没有虚构造函数的说法
3.虚函数重写
  1. 子类重写了父类的虚函数,则父类中所有同名方法将被隐藏,在子类内无法直接调用,调用需加父类作用域 (但是父类指针指向子类对象时还是可以调用,因为虚函数表中还是存的该对象)
  2. 如果父类内有函数名重载,在子类内应重写所有版本的函数(c++11提供了using语法可以在子类中直接拷贝父类的方法(816页))
  3. 如果返回类型为父类指针或引用,则重写可以返回子类的指针或引用,称之为返回类型协变
  4. 关于重写虚函数使用override关键字的好处
    1. 可以让编译器帮忙检测重写函数的语法错误,不会变成一个新的函数
    2. 可以显示的告诉其他人这是一个重写而不是重新定义的虚函数
12.内存职责划分 (涉及:默认构造 拷贝构造 赋值 析构 移动构造/赋值)
  1. 关于对象中有对象的嵌套问题,包括继承和组合 (示例519-522页)
  2. 当A类和B类中都有动态内存分配时
    1. 继承
      1.构造和拷贝构造使用初始化链表先调用父类对应的构造函数
      2.子类的赋值需要使用父类的作用域显示的调用父类的赋值
      3.析构会自动完成所以在析构函数中子类只需要管好自己的动态内存就好
    2. 组合
      1.构造和拷贝构造使用初始化链表先初始化成员对象
      2.赋值不用加作用域直接调用
      3.析构需要手动调用
  3. 如果B类没有动态内存分配则都可以使用编译器提供的默认函数(只有构造还是需要使用初始化链表),其他函数编译器提供的默认函数会自动调用A类的对应方法

7.STL

1.杂
  1. stl的算法都是[ )左闭右开的区间
  2. 存入容器内的元素必须是可默认构造、可以拷贝构造的、可赋值的、可比较的(关联式容器)
2.概念
  1. 容器、迭代器、算法、仿函数、适配器、配置器
  2. 迭代器提供能够遍历容器的对象,是广义的指针
  3. 为什么要使用迭代器,是使得算法独立于容器
  4. 迭代器分为输入输出/正向/双向/随机访问
3.容器的分类
  1. 序列容器
    vector: 在尾部插入O(1) 头部插入O(n),衍生类型stack(栈)、priorityqueue_(排序了的vector)
    deque: 相比vector多了个中控器,头插入也是O(1),衍生类型queue(队列)
    list: 不提供随机迭代器,衍生类型forward_list只支持正向迭代器
    array: 非stl容器,因为长度固定,但是可以使用stl的部分算法
  2. 关联容器 key value类型,通过key找value,内部结构是某种树
    setmultiset: key和value相同
    mapmultimap: key和value不相同
  3. 无序关联容器,内部是hash
    unorderedset_,unorderedmultiset_: key和value相同
    unorderedmap_,unorderedmultimap_: key和value不相同
4.迭代器失效
  1. erase: 删除元素会导致指向该元素的迭代器失效,erase的返回值是指向被删除元素后一个元素的迭代器,如果删除的是最后一个元素返回值为end()
    std::vector<int> vec = {1, 2, 3, 4, 5};
    auto it = vec.begin();
    auto it2 = it
    ++it;                            //it指向2
    it2 = vec.erase(it);             //删除元素2
    std::cout << *it << std::endl;   //未定义行为 it已失效
    std::cout << *it2 << std::endl;  //it2指向 原先it后面一个元素 3
  2. insert: 插入元素可能会导致容器扩容,intert的返回值是指向被插入元素的迭代器
  3. push_back: 插入也可能会导致原先使用的迭代器失效
  4. resize: 调整容器大小
  5. clear: 删除所有的元素

8.c++11

1.杂
  1. 使用{}来进行初始化
    int x{100};
    int *p = new int[4] {1,2,3,4};
  2. auto 自动推导类型 for(auto &r : vi)
  3. decltype将变量声明为表达式相同类型
  4. 返回类型后置
  5. 模板别名using =,示例 using arr12 = std::array<T, 12>; arr12 a1;
  6. nullptr 空指针
  7. 枚举添加class前缀区分
  8. 如果类中写了一个构造函数那么编译器就不会添加其他版本的构造函数,c++11使用default可以显示的声明这些方法的默认版本
    A& operator=(const A &a) = default;
2.智能指针 nique_ptr/share_ptr/weak_ptr
3.右值引用
  1. 目的: 右值引用的目的是安全的浅拷贝,让编译器知道什么时候进行安全的浅拷贝
  2. 语法
    A(const A &a);             //拷贝构造
    A(A &&a);                  //移动构造(安全的浅拷贝)
    A& operator=(const A &a);  //赋值
    A& operator=(A &&a);       //移动赋值
    A a1,a2; a2=std::move(a1); //对左值强制使用移动赋值,如果没有定义移动赋值则调用赋值
  3. 移动构造函数: 类会默认的提供移动构造,如果被包含有类定义了移动构造会调用被包含类的移动构造方法,如果被包含类没有定义移动构造那就调用拷贝构造。(移动拷贝同理)
4.lambda
[&count] (int x) {count += (x % 13 == 0)}; //被13整除计数
[]代表函数所以是匿名的
{}函数体
[&] 按引用传递
[=] 按值传递
[a, &b] a按值传递,b按引用传递 
[=, &x],[&, x] 除了x其他的按值传递,除了x其他的按引用传递
文档更新时间: 2026-03-31 17:35   作者:morninglu