C++的崛起之路,誓要成为C++全能!(边刷边学,一篇文章持续追加)
1.字符型变量赋值C/C中反斜杠不可以作为字符常量单独使用反斜杠是转义字符的开始标记字符型变量的赋值规则1反斜杠不能单独使用\\表示\自身2单引号赋值时只能有一个字符3整数赋值时要注意范围unsigned 0~255 signed -128~1274双引号不能赋值“a”是const char*,不能用字符串给字符常量赋值5注意八进制和十六进制的赋值 八进制\oooooo是1-3位0~7的数字十六进制\xhhhh是1~2位0~F的数字2.C23 deducing this显式对象参数和CRTP1deducing thisC23之前如果想写一种成员函数支持左值常性左值右值常性右值四种调用必须写四个重载函数明明逻辑都相同却要写四遍维护起来非常冗余所以引入了deducing this调用方式self被推导为的类型普通左值Widget w; w.get_value();Widget左值引用const左值const Widget cw w; cw.get_value();const Widgetconst 左值引用右值Widget{}.get_value();Widget右值引用const右值std::as_const(Widget{}).get_value();const Widgetconst 右值引用deducing this就是用一行代码实现const和非const,左值右值的类型转换解决了成员函数冗余的问题2CRTP奇异递归模版模式让派生类把自己的类型作为模板参数传给基类基类再通过静态转换拿到派生类的实例从而实现编译期的多态行为。在 C23 引入 deducing this 之前很多场景都得靠它来实现它的核心是基类知道自己被哪个派生类继承了能安全地调用派生类的成员又不用写虚函数。CRTP 解决了什么问题它是为了替代虚函数实现的动态多态带来两个核心优势零运行时开销虚函数需要虚函数表、运行时动态绑定而 CRTP 是编译期静态绑定没有额外开销。可以访问派生类的成员普通基类不知道派生类的存在CRTP 让基类能 “安全地知道派生类是谁”从而调用派生类的方法。3.C14 std::exchange1为什么会有移动语义在C11之前所有的对象都是深拷贝std::vectorinta{1,2,3}; std::vectorintba;//必须在堆上重新申请一片堆上的内存然后将a复制过去如果对象很多会带来巨大的性能开销但是有些时候我们只是想把临时对象的资源拿过来根本不需要拷贝所以C11引入了移动语义核心思想把对象资源堆内存文件句柄等从一个对象移动到另一个对象不做拷贝2右值引用移动构造/移动赋值什么是左值可以取地址的有名对象eg.变量a什么是右值不可以取地址的无名对象临时对象 eg.std::vectorintb{2,3,4};移动构造/移动赋值的作用接管右值对象的资源class Base{ private: int val; public: //移动构造从other中获取资源 Base(Baseother)noexcept;//noexcept:是保证不抛异常 //移动赋值从other中获取资源 Base operator(Baseother)noexcept; }移动语义的核心要求被移动的other必须处于绝对安全的空状态NULL这样析构时不会重复释放资源3完美转发std::forword和auto完美转发的作用:让函数模版能完美保留参数的右值和左值属性把参数原封不动的转发给其他函数当T是模版时T是万能引用可以随意匹配左值和右值其他时候就是右值引用传入左值T就会被推导为左值引用传入右值T就会被推导为右值引用std::forword的作用让std::forwordT(arg);将arg转化为本来的类型void other_func(int x) { cout 收到 xendl; } templatetypename T void func(T arg) { other_func(std::forwardT(arg)); } int main() { func(2); }4std::exchange到底是什么定义在头文件#includeutility下template typename T, typename U T T std::exchange(T obj, U new_value) { // 1. 把 obj 的旧值移动出来避免拷贝 T old_value std::move(obj); // 2. 把 new_value 转发给 obj给它赋新值 obj std::forwardU(new_value); // 3. 返回 obj 的旧值 return old_value; }实现单侧转移先将obj的旧内容移动给一个新参数再将new_value移动给obj最后返回obj,使用移动语句和完美转发避免不必要的拷贝特性std::exchange(obj, new_val)std::swap(a, b)操作方式单向赋值 返回旧值双向交换两个对象的值修改对象只修改obj修改a和b两个对象适用场景所有权转移移动语义交换两个对象的状态5题中的代码如果不使用std::exchange,就需要这样struct Buffer { int* data; size_t size; Buffer(Buffer other) noexcept : data(other.data) , size(other.size) { other.datanullptr; other.size0; } };4.C 虚函数的核心规则1派生类中继承基类虚函数不需要virtual2派生类中建议用override显示标记重写虚函数3虚函数不能是静态函数也不能是全局函数友元除外①为什么虚函数不能是静态成员函数虚函数的核心是动态绑定多态Base* p new Derived(); p-func(); // 运行时根据p指向的对象类型决定调用Base::func还是Derived::func它的实现依赖两个东西虚函数表vtable每个类都有一张表存着所有虚函数的地址。虚函数指针vptr每个对象里都有一个指针指向它所属类的虚函数表。调用p-func()时会通过p指向的对象的vptr找到虚函数表再调用对应的函数。静态成员函数的本质静态成员函数是属于类而不是属于对象的函数它没有this指针也不依赖任何对象存在。所有对象共享同一个静态成员函数调用时不需要对象。两者为什么冲突虚函数的调用必须通过对象的vptr来查找虚函数表但静态成员函数不属于任何对象没有this指针也就拿不到vptr。无法参与动态绑定所以不能被声明为虚函数。②为什么虚函数不能是普通全局函数友元的情况是特例普通全局函数的问题普通全局函数不属于任何类自然也没有this指针无法参与基于对象的动态绑定所以不能是虚函数。友元函数的特殊情况友元函数虽然不是类的成员函数但可以被声明为另一个类的友元并且可以是虚函数吗注意友元函数本身不能被声明为虚函数但可以作为基类的友元并且通过类的成员虚函数来实现多态。“虚函数可以是另一个类的友元函数”指的是一个类的成员虚函数可以是另一个类的友元而不是说友元函数本身是虚函数。class B; class A { public: virtual void func(B b); // 成员虚函数同时可以是B的友元 };这个func是A的虚成员函数同时可以被声明为B的友元从而访问B的私有成员。4纯虚函数必须在派生类中实现否则派生类也是抽象类无法实例化①什么是纯虚函数纯虚函数的声明virtual void func() 0;②纯虚函数的作用纯虚函数让基类变成抽象类强制派生类实现这个函数保证派生类都有统一的接口。③派生类是否需要实现虚函数取决于派生类是否需要实例化派生类需要实例化必须实现基类的所有纯虚函数否则派生类也是抽象类无法创建对象。class Base{ public: //纯虚函数的声明 virtual void func()0; }; class Derived:public Base{ public: void func()override{ cout实现endl; } }; int main(){ Derived b;//实例化创建对象b }派生类只是作为基类不需要实例化可以不实现纯虚函数派生类仍然是抽象类只能被继承不能创建对象。class Base{ public: //纯虚函数的声明 virtual void func()0; }; class Derived:public Base{ //没有实现纯虚函数 }; //不能实例化纯虚函数强制派生类实现接口如果派生类要实例化就必须实现所有纯虚函数否则它也是抽象类不能创建对象。5.C STL 容器与算法的核心特性6.数组名和指针变量s是一个数组名代表的是数组首元素的地址常量不可以被修改p是一个指针变量指向数组首元素的地址可以被修改所以p1;//指向数组第二个元素*p;//的优先级高与*所以相当于是*(p); //指向数组第二个元素然后解引用1((*p));//指针指向数组第一个元素然后解引用之后1数组名s是 “只读的地址”不能改指针变量p是 “可读写的地址”可以移动7.构造函数的析构顺序以及构造函数和成员函数的初始化顺序先构造的后析构后构造的先析构先声明的先初始化8.大小端存储大端序高字节存低地址内存地址从低到高排序地址索引0123字节值0x110x220x330x44小端序低字节存低地址地址索引0123字节值0x440x330x220x11联合体判断大小端存储union x { int a 0x00000001; char b; }; int main() { union x t; t.b; if (t.b 1) { cout 小端存储; } else { cout 大端存储; } }9.结构体和指针注意先p还是p,这个很重要10.二叉树结构和遍历顺序11.宏定义宏是纯文本替换不做任何额外处理宏#define SQR(A) A*A只是简单的文本替换不会先计算A的值再代入。当你写SQR(y z)时预处理器会直接把它替换成y z * y z而不是(y z)*(y z)