美文网首页C++C++C++
C++的一些容易忘记的知识点

C++的一些容易忘记的知识点

作者: 梅花怒 | 来源:发表于2018-11-26 21:40 被阅读82次
    • 头文件中一般放置四样东西:声明、宏、static定义、inline定义。
    • 预处理是把x.h(或者其他任何的文件后缀。包括.hpp .wadwasd等等随便的名字)文件复制到任何书写了#include"x.h"的位置。编译是编译器对于源程序进行词法分析、语法分析、语义分析以及生成汇编代码的过程。对于每一个编译单元,编译器独立的去编译,各个编译单元互不影响。汇编是把汇编代码转化成对应的二进制机器代码。连接阶段是把各个编译单元产生的二进制代码连接起来。
    • 一般来说,.h文件中都是一些声明,这样,一个文件include它之后,就相当于include进来一大堆声明,在连接的时候就可以发挥作用。如果.h文件中有变量或者函数的定义,那么多个文件include这个.h文件之后,在连接阶段就会发生重复定义的错误。
    • static的变量和函数是只在当前的编译单元可见的,连接器也看不见static的变量和函数。那么在.h文件中就可以定义static的函数和变量,然后多个文件就可以include进来,并且互不干扰,连接器看不见,就不会管。这样做的好处是,编译器可以进行内联优化。也就是说,如果在同一个编译单元中有一个函数的定义和调用,那么这个调用就可能会转化成函数在这个地方展开之后的结果。这样就省了函数调用(call和ret和保存现场)的开销。
    • 但是static函数如果要是很大,不能内联优化的话,那么每一个include进来的文件就都有一个函数的定义。这会让汇编代码很庞大。
    • 所以c++提出了inline。连接器是可以看见inline变量(c++17)和函数的。我们可以在头文件中写inline函数,这样,如果inline函数可以被内联优化的话,那么和static函数的形式差不多。而如果inline函数不能被内联优化的话,那么连接器只会保留inline函数的一个副本。
    • char的符号是不一定的。
    • 私有继承不具有子类自然转化为父类的转换关系,只是代表了和组合一样的含义。
    • char可能有符号可能没有符号。注意有符号和无符号类型的溢出。注意有符号和无符号做运算,有符号转化为无符号。
    • 字符串字面量中间有空白(空格,tab,换行),实际上则是一个整体。
    • 初始化和赋值不一样。初始化是创建变量时(定义时)赋予其初始值,赋值是把当前值擦除,用新值替代。一律用花括号初始化,避免“函数声明陷阱”。
    • T&不能和字面量绑定,const T&则可以和字面量绑定。
    • 顶层const:对象本身是常量。底层const:指向的东西是常量。引用只有底层const。因为实际上,int&就相当于int* const了。
    • constexpr是指在编译期间有可能可以确定的值,可以修饰变量、函数返回值、if语句。
    • auto推导出来的类型一定是对象类型变量。auto不能推导出引用类型。auto也不能推导出const类型。必要时必须要const auto&来声明类型。
    • decltype(左值表达式)返回引用。decltype(变量)返回变量正常类型。
    • c++11可以指定类内初始值。
    • 预处理器变量无视作用域,取消要用#undef。
    • string忽略“ balaba”这之间的空白,从b开始识别。
    • for(auto& c : arr)要用&引用,这样才可以通过c来修改arr。如果for(auto c : arr)则有n个拷贝函数的调用,把arr里的值分别赋值给c。
    • 不可以边改变容器大小边遍历。
    • vector下标只能访问或者修改,不能无中生有去添加。
    • 容器迭代器用!=而不是<或者<=,因为不是所有的容器都有<或者<=,但是都有==和!=。
    • auto x = arr,x为指针,decltype(arr)为int[]。
    • 数组下标可以为负数,然而vector和string则只能是自然数。
    • string(“hahaha”).c_str()返回const char*类型。
    • 多维数组for(auto& x : arr)所有外层必须要用&引用。带引用的话,x就是一个数组的引用,即int(&type)[4],可以展开。如果不用引用,x就变成了指针,例如int*,而内层再for(auto y : x)的时候,for不能展开一个int*,所以出错。
    • 运算符表达式求值顺序并不确定,只有4种是确定的:&&和||和?:和,。
    • c++11规定负数除法向零取整。
    • 位运算符用于无符号类型。有符号类型的位运算符没有规定。
    • sizeof返回size_t类型,无符号整数,注意sizeof和其他int类型的值计算的时候,int会隐式转化成size_t。
    • sizeof可以操作野指针,甚至可以对野指针解引用。因为对sizeof的求值是编译期的。甚至sizeof(++i)里面的++i也有可能不被执行。
    • sizeof(arr)会把整个数组大小返回,而不是把arr看成指针或者地址。
    • 逗号表达式的值是右侧的结果。(a, b)返回b。
    • 显式转换有4种:static_cast,dynamic_cast,const_cast,reinterpret_cast。其中const_cast去掉的是底层的(只有指针和引用有的那个底层的const)。其中,static、const、reinterpret的转换区别在于编译期对被转换对象的类型检查,在汇编代码就是直接把对象移给别人,没区别。而dynamic转型则是在运行期间的汇编代码调用函数的。
    • else就近匹配。
    • case语句里可以定义变量,但是不能初始化(隐式初始化也算初始化)。如果非要做这样做的话,可以加上{}。
    • for语句第一句可以声明多个对象,但是类型要相同,例如int i = 0, j = 2;
    • do while有分号。do{} while(cond);。C语言宏可以这样定义#define xx do{...}while。这样调用宏的时候就可以xx;了。
    • try内的变量,catch中无法访问。
    • 函数调用时对实参求值顺序不固定。
    • 函数不能依赖形参的顶层const来重载。
    • 数组传参decay。即:auto f(int a[])等价于auto f(int* a),并且auto f(int a[][4])等价于auto f(int(*a)[4])。其中int a[][4]的意思就是a是一个int[4]类型的数组,所以转化成a是一个int[4]类型的指针。
    • argv的实参从argv[1]开始。argv[0]是程序的名字。
    • 可变形参的3种方式:1.所有参数都一个类型:initializer_list<T>。2.不同类型,用可变参数模版template <typename... Args>。3.省略符(C语言里的)auto f(int a, ...)。
    • void返回值得函数可以返回一个(返回void的函数调用)。即可以return g();
    • 值返回A f()这种函数,返回的时候会有一个copy调用。如果是引用返回,A& f()这种函数,则返回的时候不会有任何调用。
    • 底层const(指针,引用)可以重载。
    • 在不同作用域下,无法构成重载,内层作用域函数把外层屏蔽。
    • constexpr修饰函数的返回值且生效的时候,这个函数只能有一个return,一切必须是确定的。
    • decltype作用于函数名,返回的是函数类型而不是指针类型。
    • =default,=delete。
    • 类内函数是否是const this的可以区分重载。
    • 友元不可以传递。
    • 友元函数可以定义在类的内部。
    • 用::var表示类外的作用域。
    • 构造函数的初始化列表里才可以初始化变量。花括号内的都是重新赋值。拷贝构造函数也有初始化列表,只要是A(...)类型的函数都有,但是operator=就没有。
    • 初始化顺序依赖于类内声明变量的顺序,和列表初始化的顺序无关。
    • explicit防止构造函数传参的时候隐式转换。
    • constexpr构造函数必须是空的,而且要在列表中初始化所有成员。
    • 要在类外定义类内的static变量。但是可以用static constexpr int初始化变量,然后再在类外定义一下(空定义)。
    • 类内static成员可以用不完全类型,因为首先类内的static成员不在类的内存里,其次static成员知道类多大。
    • <<flush刷新,<<ends输出空格和刷新,<<endl输出换行和刷新。
    • cout << unibuf设置立即刷新,nounitbuf回到正常缓冲方式。
    • cin时,自动刷新cout。
    • cin.tie(&cout)关联流。即cin之后默认刷新cout。可以用cin.tie(nullptr)解绑,加快速度。
    • vector短的比长的小。
    • 单向链表不支持size。
    • 放入容器中的都是拷贝,不是真的把原对象放进去。
    • insert插到iterator之前,返回指向插进去的新的元素的iterator。
    • insert返回的是迭代器指针,可以循环插入iter = insert(iter)。
    • emplace直接调用构造函数,而且自动匹配构造函数。而不调用拷贝函数。
    • lambda有值捕获和引用捕获。可以隐式捕获[=]或者[&]或者[=,&a,&b,&c]或者[&,a,b,c]。
    • mutable是内部拷贝了一个原始值的量并且可以修改。但没有mutable,捕获的就不是可以修改的值。而&则是一直和变量绑定在一起,但是mutable并不是。
      对于lambda表达式
      [&a, =b](int x, int y) -> double { return x + y + a + b; }
      就等价于
    class Unnamed
    {
    private:
        double& a;
        double b;
    public:
        Unnamed(double& A, double B) : a(A), b(B) {}
        double operator()(int x, int y) const { return x + y + a + b; }
    };
    

    即,lambda等价于一个类,类里面有(成员变量 | 构造函数 | operator() const)。其中捕获的值就是成员变量,用局部变量来初始化它们。注意mutable的意思是,operator() const 变成 operator(),没有const。

    • mutable的本意是,这个mutable int x变量在一个const的对象里面也可以被修改,即:可以被const this的函数修改,也可以当做const A中的一员然后A.x修改。
    • insert插入可重复时,返回迭代器指向新的元素。insert插入不可重复时,返回一个pair,first为迭代器,second为是否成功的bool。
    • equal_range得到的pair是包括lower和upper的。
    • 对于new,对于内置的数值类型,没有括号代表不初始化,有括号代表默认初始化。对于类A,有没有括号是一样的,都默认初始化。
    • 可以用new分配const对象,const int* p = new const int(1024);
    • 可以用auto初始化,auto x = new auto(obj)。代表先拷贝一份obj2,调用拷贝构造函数,然后让x指向obj2。
    • 动态数组可以是空的,即int* a = new int[0];合法,返回空指针。
    • unique_ptr可以管理动态数组,即unique_ptr<int[]> u(new int[5]);可以正确释放内存。但是shared_ptr不能,必须自己写删除器并且传入。即shared_ptr<int> sp(new int[10], [](int* p){delete[] p;});
    • allocator的目的是为了把“分配内存”和“创建对象”两件事分开。allocate分配内存,deallocate释放,construct构造,destroy析构。
    • 拷贝构造函数可以有多个参数,但是2到n个参数之间要有默认值。
    • 拷贝构造函数默认实现是值拷贝。
    • 拷贝构造函数的参数必须是A(const A& a),如果是(A a),那么传参的时候依旧调用拷贝构造函数,无穷递归。
    • 赋值函数必须是 A& operator=(const A& a),返回必须引用,即为左值,为了连续赋值。
    • 不能=delete析构函数。
    • 赋值函数要保证自赋值是安全的,同理交换函数也需要。
    • 右值引用必须引用在右值上,不能引用在左值上。
    • ++i返回左值,i++返回右值。
    • std::move的唯一作用(语义)是把左值变成右值。
    • 移动函数不分配任何内存,只是内存的接管。A(A&& a) noexcept,声明和实现都要说noexcept。
    • 移动函数一定要把a里的指针都置成nullptr,不然会释放this里的资源。
    • 移动赋值和移动拷贝类似,返回A&,参数A&& a,要noexcept。
    • 如果显式定义移动和移动赋值,那么默认的拷贝和赋值则是delete的。
    • 引用限定符加在成员函数后面,&或者&&代表这里的this是左值还是右值。
    • 引用限定符和const this都可以区分成员函数的重载。
    • 如果用了引用限定符,那么参数列表相同的重载函数都必须用引用限定符。
    • 前置++--的重载是返回引用,没有参数。后置多一个额外的int形参,没什么用只是为了区分,返回拷贝。
    • *的重载:A& operator*(),*返回引用
    • ->的重载:A* operator->(),->返回指针
    • A->a,如果A是指针那么直接调用,如果A是对象,那么等价于(operator->())->a
    • 每一个lambda的类型都是不一样的。
    • 重载函数要想装入function,必须消除二义性,可以封装lambda或者用函数指针。
    • 转换函数:operator B() const
    • A a = b或者A a = {}是初始化,初始化调用构造函数;而A a是定义,调用构造函数;a = b是赋值,调用赋值函数。
    • 定义是占用内存空间。初始化是设置内存空间中的值。声明是声明一个变量可以用。赋值是修改值。
    • 在构造函数中,initialized_list<int>和int可以构成重载,注意A(5)调用int,A{5}调用initialized_list<int>,A({5})调用initialized_list<int>。
    • b想变成a两种方法:“b的转换函数返回a”和“a的构造函数有参数b”,有二义性。
    • 在虚函数末尾加override关键字,和java的@override一个意思。
    • 构造函数不能是虚函数,因为构造函数是构造出一个明确的对象,不需要多态,而且是层次调用的。析构函数一般是虚函数,为了基类指针delete的时候可以正确调用析构函数。
    • 注意,析构函数的名字是(dtor),而不是(~类名)。所以可以override。
    • 静态函数不能是虚函数。因为虚函数放在表里,而不是静态区域。
    • 子类构造函数列表可以显式调用基类的构造函数。如果不写,调用默认无参数的父类构造函数。所以如果子类拷贝函数不在列表中调用父类的拷贝函数的话,父类的成员就会调用默认的构造函数来初始化,导致语义错误。
    • 先初始化基类成员,再初始化子类成员。析构反过来。
    • 没有子类的类,定义写final。即class A final : public B{}这样写,别的类不能继承A。
    • final也可以修饰虚函数,virtual void f() final。final的虚函数可以覆盖上一版,但不可以再被覆盖。
    • 虚函数的默认实参遵循静态绑定而不是动态绑定。
    • 作用域限定符可以强制指定调用哪个虚函数。
    • 纯虚函数virtual int f(int) = 0;在声明的时候写=0,然后不定义。可以在类外为纯虚函数提供函数体,而不能在类内提供。
    • 在类内用using可以改变访问权限,例如class D : private B { public: using B::size; };把本来是private的B::size变成了public。也可以声明一个基类的非虚函数using B::func来构成重载。
    • 不同作用域的函数不构成重载,并且寻找名字的时候找到对应的名字就不再找了,找名字的过程不参考形参列表,所以即使内层不匹配,而外层有匹配的,依旧调用内层然后报错。
    • 在构造和析构函数中调用虚函数调用的是构造析构函数所属类型对应的虚函数版本。注意在构造和析构函数中,子类都是不完整的。
    • 构造函数可以继承,即子类可以直接使用基类短的构造函数。这样写:using B::B就等价于D(params) : B(args) {},其中D多余的(独有的)变量会被默认初始化。
    • 模版里只能是整型,不能是double或者其他非整型类型。
    • 模版编程尽量保证最大的类型无关性。比如参数要const T&或T&&,比较尽量用<。
    • 在模版作用域内,A和A<T>是等价的。
    • 可以将模版参数声明为友元,即template<typename T> class A { friend T; };注意是friend T不是friend class T。
    • 可以定义模版类型别名变量,即template <typename T> using twin = pair<T, T>;这样,这种用法只能是using,而不能是typedef。然后twin可以这样用:twin<string> name。也可以定义模版变量(c++14),即template<typename T> T t = T(args);
    • 要想用类名访问static变量,必须要有一个模板实例,没有模板实例是不会产生这个变量的。
    • A<T>::X这样调用,默认X是一个static变量,而如果X要是类型的话,必须这样调用:typename A<T>::X。
    • 成员模版函数不能是虚函数,因为这种函数有没有是不一定的,但是虚函数表要确定。
    • 类模板必须自己指出,而函数模版可以自动推断。
    • template class B<int>;可以显式的实例化一个类型的模板,并且实例化所有操作,其他用到这个实例化的地方可以extern template class B<int>;来防止重复的实例化。
    • 返回值是模板参数类型的不能自动推断必须指定类型。可以只指定左侧的一些模板参数,右侧的没指定的自动推断。
    • 尾置返回类型搭配模板可以不用指定返回值类型,可以auto f(T t, G g) -> decltype(*t)。同时可以搭配type_traits对类型的操作。
    • 引用折叠,只有两个右值引用变成右值,其他都变成左值引用。所以T&&可以接受任何类型实参,因为T可以推断为T和T&。
    • forward把类型T的变量t变成右值引用T&& t,加了两个&&,但是注意要有折叠,如果forward<T&>(t)则变成T&。而move则是就是变成右值,无论值是什么。所以forward类型相关,要用模版调用,而move并不类型相关,传递给move一个变量就够了,左值右值统一处理。但是要注意两者调用的时候必须必须std::move和std::forward。
    • 模版匹配规则先匹配非模版,如果不需要转型那么就匹配,如果需要转型就放弃,转而去衡量模板函数。(函数<->指针<->数组)的转化都是精确匹配,顶层const的转化也是精确匹配。
    • 可变参数模板,template<typename T, typename... Args> void f(const Args&... args);
    • sizeof...可以应用于args和Args,分别获取数目。
    • 3种new:1.plain new,A* a = new A(args);完成了两个操作,一个是申请动态空间,一个是调用A的构造函数。2.operator new,就是malloc的简单封装,只申请动态空间,void* p = operator new(sizeof(int));。3.placement new:在已经有的空间上构造对象,A* a = new (p) A(args);。placement new可以在栈上构造对象。
    • c++的多态主要成本在于1.虚函数,2.虚继承,3.多重继承指针的转换(指向第二个父类的时候)。
    • 拷贝函数的调用时机有三种可能1.显式初始化,即显式地调用。2.函数值传递传参。3.函数值传递返回值。
    • 空类大小是1个字节。编译器安插进去一个char,使得两个对象可以在内存中拥有独一无二的地址。
    • static变量一定要在类内声明,类外定义(以及初始化)。即
    class A { public: static int x; };
    int A::x = 2;
    

    因为声明什么也不干,但是定义是分配内存,static的内存分配在全局区(静态区),和class不是一个区的,static和class的实例没有任何血缘关系。注意定义的时候不能再说static。

    • 成员函数的参数的类型确定是即时的,而对于函数的本体的分析却延迟到整个class的声明完成之后。所以在类内的typedef的地方靠后可能会导致成员函数参数的不正确。
    • 成员函数实际上有隐藏的第一参数(A* const this)
    • 补齐的时候,子类里面的父类自己补自己的,而不是和子类的成员一起补。因为向父类转化的时候,要保证父类的完整性。
    • 多重继承B2* b2 = d;要转换成B2* b2 = d ? (B2*)((char*)d + sizeof(B1)) : 0;即offset必须要改变,指针要向后移动sizeof(按照声明顺序先继承的父类们的大小)。要删除对象时,B2* b2 = d;必须把指针给调回来,回到对象的初始位置。
    • 多重继承就有多个vfptr。
    • 成员函数的调用会转化成全局的调用。即每个函数添加一个(A* const this)的参数,并且改名。即调用a.f();转化成A_f(&a);p->f();转化成A_f(p);。而对于虚函数的调用p->f();则转化成(*p->vfptr[5])(ptr);。而对于静态函数,则直接改名,什么都不干。
    • 逗号表达式可以逗很多个,A,B,C都可以,但是返回值就是最后一个分句的值,即C的值。
    • 可以在类外定义纯虚函数的函数体并且可以用类名A::pure()调用纯虚函数。对于纯虚的析构函数,必须要定义,没有定义以后继承的话就没办法调用父类的析构函数了。即,析构函数可以是纯虚的,但必须要定义纯虚的析构函数。
    • 初始化vfptr的时机在构造完所有基类之后,且在其他代码之前。
    • 在拷贝和赋值操作的时候一定要考虑我拷我自己和我赋我自己的情况。
    • 虚继承的情况,构造函数多一个bool参数。如果D继承B和C,而B和C虚继承A,那么B和C都不调用A的构造函数(即bool参数为false),而D调用A的构造函数。
    • 每次new都需要传回独一无二的指针,所以new T[0]的返回值指向一块内存为1的块。
    • 不要用B* b = new D[10];这样当delete[] b;时,内存错乱了就,不能在正确的位置调用析构函数。
    • 地址空间从低到高分别是:代码段、数据段、BSS段、堆、mapping段、栈。其中代码段和数据段是从磁盘中加载的,而BSS段是运行时在内存中开辟的,BSS段不存在于磁盘中。
    • static的意思是“不可以被其他文件访问的全局变量或函数”,存放在数据区或者BSS区中,存储在哪个区取决于static变量是否被初始化了。而static的类成员变量的意思则是“没有A* const this指针的、和对象无关的函数”。
    • inline的意思是“可以允许不同的编译单元重复定义的全局变量或函数”。(inline变量在c++17引入)。
    • c++11可以用call_once实现单例。

    相关文章

      网友评论

        本文标题:C++的一些容易忘记的知识点

        本文链接:https://www.haomeiwen.com/subject/ggliqqtx.html