update 2025.03.01,对实际情况中遇到的内容使用📌标记。
一.关键字/基础知识 CONST 1.作用
定义常量,防止修改,起保护作用。
类型检查:#define不会进行类型检查,const则是具有类型的。
节省空间:常量只有一份数据,可以避免多份拷贝。
2.默认为文件局部变量
const 对象默认为文件局部变量,如果需要在其他文件中访问,则需要extern定义:
1 2 3 4 5 6 7 #include <iostream> extern const int ext = 2 ; int main () {...}extern const int ext;int main () {...}
3.常量在定义时必须被初始化
4.常指针和指向常量的指针
const int 为指向常量的指针, int const为常指针。
指向常量的指针可以指向非常量,但不能修改。
常指针必须初始化且不能修改,常指针不能指向常量,如果需要则必须为const int * const形式。
5.函数中使用const
const指向参数或常指针参数无意义,因为本来就是临时变量拷贝的。
可以为指向常量的指针。
可以为引用。
6.类中使用const
const成员函数不能修改成员或调用非const成员函数。
const对象只能访问const成员函数。
const成员变量必须初始化列表初始化,C++11支持定义处初始化,或者可以结合static进行定义处初始化。
1 2 3 4 5 6 7 8 9 class Apple {private : int people[100 ]; public : Apple (int i); const int apple_number; }; Apple::Apple (int i):apple_number (i){}
STATIC 1.C语言中的STATIC
STATIC修饰的全局变量/函数只能在本文件使用,其他文件无法直接调用。即无外部链接性。
STATIC修饰的局部变量必须初始化且仅初始化一次,在程序编译时确定初始值,程序运行结束后才销毁。
2.C++中的STATIC
类中的静态变量由对象共享。不能使用构造函数初始化,也不能在类中初始化,📌只能在外部进行初始化。
类中的静态函数只能访问静态变量。
📌工厂单例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 namespace base{class CPUDeviceAllocatorFactory { public : static std::shared_ptr<CPUDeviceAllocator> get_instance () { if (instance == nullptr ){ instance = std::make_shared <CPUDeviceAllocator>(); } return instance; } private : static std::shared_ptr<CPUDeviceAllocator> instance; }; } namespace base{std::shared_ptr<CPUDeviceAllocator> CPUDeviceAllocatorFactory::instance = nullptr ; }
THIS this指针指向类对象本身,通常用于返回对象本身,或在参数和成员变量名相同时使用。
INLINE inline可以避免函数调用的开销,但会导致代码膨胀,因此只在函数非常短时使用。
虚函数可以是内联函数,但只有在已知对象是哪个类时可以内联。使用指针访问时表现多态性,不能调用内联的虚函数。
SIZEOF
空类的大小为1字节。
类中的函数和静态数据成员不占用对象存储空间。
继承时,派生类继承基类成员,按照字节对齐计算大小。
继承多个有虚函数的基类时,继承多个虚指针。
纯虚函数和抽象类
📌纯虚函数通过赋值0来声明。包含纯虚函数的类为抽象类。
所有纯虚函数被覆盖,类才不会变为抽象类。
纯虚函数没有实现,虚函数必须实现。
1 2 3 4 class Shape {public : virtual void draw () = 0 ; };
虚函数
友元不是成员函数,不能是虚函数,但是可以让友元函数调用虚函数,实现友元的虚拟。
虚函数是通过vptr实现多态的。每个使用虚函数的类都有一个vptr,指向虚函数表,表中为函数指针。当使用基类指针调用虚函数时,通过vptr找到子类的虚函数表调用子类的函数实现。子类的虚函数表中没有重写的虚函数都仍然为基类指针,重写的为指向重写函数的指针。
📌构造函数不能是虚函数,析构函数可以是,并且析构函数应该是虚函数 ,调用相应类型的对象的析构函数,然后调用基类的虚构函数。如果不是虚函数,使用基类指针进行delete时,就会导致派生类析构未调用,释放不正确。例如以下代码,基类析构就必须是virtual的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <iostream> class Base {public : virtual ~Base () { std::cout << "Base destructor" << std::endl; } }; class Derived : public Base {private : int * data; public : Derived () { data = new int [100 ]; std::cout << "Derived constructor" << std::endl; } ~Derived () { delete [] data; std::cout << "Derived destructor" << std::endl; } }; int main () { Base* ptr = new Derived (); delete ptr; return 0 ; }
函数的默认参数是静态绑定的,因此默认参数是根据指针或引用使用的,而不是对象类型。例如base指针调用函数,使用的是基类的默认参数。调用的是指向对象的虚函数实现,取决于实际的指向对象。
静态函数不能是虚函数,因为他不属于任何类对象。
虚函数私有:要把基类声明为public,继承类为private,就可以用指针正常访问。
VOLATILE
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)。
const 可以是 volatile (如只读的状态寄存器),指针可以是 volatile。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 volatile bool bStop=false ; void threadFunc1 () { ... while (!bStop){...} } void threadFunc2 () { ... bStop = true ; }
ASSERT 断言主要用于检查逻辑上不可能的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> #include <assert.h> int main () { int x = 7 ; x = 9 assert (x==7 ); return 0 ; }
在代码开头加上#define NDEBUG可以忽略断言。
位域 位域必须是整型或枚举,通常使用结构体声明,为每个成员设置名称,并决定其宽度:
1 2 3 4 5 6 struct _PRCODE { unsigned int code1: 2 ; unsigned int cdde2: 2 ; unsigned int code3: 8 ; };
C语言用 unsigned int 作为位域的基本单位,即使一个结构的唯一成员为 1 Bit 的位域,该结构大小也和一个 unsigned int 大小相同。 有些系统中,unsigned int 为 16 Bits,在 x86 系统中为 32 Bits。
一个位域成员不允许跨越两个 unsigned int 的边界,如果成员声明的总位数超过了一个 unsigned int 的大小, 那么编辑器会自动移位位域成员,使其按照 unsigned int 的边界对齐。
extern “C” C++和C中对函数编译生成的符号不同,C编译的函数名类似_add,没有参数,因为C语言中没有重载。所以如果链接C和CPP文件,则需要在引用C的头文件时,加extern “C”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #ifndef ADD_H #define ADD_H int add (int x,int y) ;#endif #include "add.h" int add (int x,int y) { return x+y; } #include <iostream> using namespace std;extern "C" { #include "add.h" } int main () { add (2 ,3 ); return 0 ; }
要将.c文件编译为.o文件(gcc -c)后进行链接。
STRUCT struct名字和函数名可以相同,但是不能是typedef定义别名,而且必须在声明前加struct:
1 2 3 4 5 6 7 8 9 struct Student {};Student (){}Struct Student s; Student s; typedef struct Base1 {}B;
和类的区别为struct成员默认为public,更适合看成数据结构实现体。
UNION 联合可以有多个数据成员,任意时刻只有一个数据成员有值。默认为public,可以有构造和析构函数,不能含有引用类型成员,不能继承或作基类或含虚函数。
EXPLICIT 📌explicit修饰构造函数,防止隐式转换和拷贝初始化;修饰转换函数时可以防止隐式转换,按语境转换除外。实际上,大部分拷贝初始化都会被自动优化为直接初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 struct B { int num; explicit B (int b) : num(b){ } }; void func (B b) {}int main () { B b1 (1 ) ; func (1 ); B b2 = 1 ; B b3 = (B)1 ; }
FRIEND 友元共有两种形式,友元函数和友元类。友元没有继承和传递。
友元函数声明在类中,定义在外部:
1 2 3 4 5 6 7 8 9 10 class A {public : A (int _a):a (_a){}; friend int geta (A &ca) ; private : int a; }; int geta (A &ca) { return ca.a; }
友元类声明在类声明中,实现在类外:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class A {public : A (int _a):a (_a){}; friend class B ; private : int a; }; class B {public : int getb (A ca) { return ca.a; }; };
📌对于流重载这样的操作,不一定要使用友元,类应该有对外接口访问其成员。
1 2 3 4 std ::ostream& operator<<(std ::ostream& os, const Status& x) { os << x.get_err_msg(); return os; }
USING 作用域:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <iostream> #define isNs1 1 using namespace std;void func () { cout<<"::func" <<endl; } namespace ns1 { void func () { cout<<"ns1::func" <<endl; } } namespace ns2 {#ifdef isNs1 using ns1::func; #elif isGlobal using ::func; #else void func () { cout<<"other::func" <<endl; } #endif } int main () { ns2::func (); return 0 ; }
改变访问性:
1 2 3 4 5 6 7 8 9 10 11 12 class Base {public : std::size_t size () const { return n; } protected : std::size_t n; }; class Derived : private Base {public : using Base::size; protected : using Base::n; };
重载:在继承过程中,派生类可以覆盖重载函数的0个或多个实例,一旦定义了一个重载版本,那么其他的重载版本都会变为不可见。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <iostream> using namespace std;class Base { public : void f () { cout<<"f()" <<endl;} void f (int n) {cout<<"Base::f(int)" <<endl;} }; class Derived : private Base { public : using Base::f; void f (int n) {cout<<"Derived::f(int)" <<endl;} }; int main () { Base b; Derived d; d.f (); d.f (1 ); return 0 ; }
取代typedef:
1 2 typedef vector<int > V1; using V2 = vector<int >;
ENUM 枚举有一个问题是作用域不受限,会引起命名冲突:
1 2 enum Color {RED,BLUE};enum Feeling {EXCITED,BLUE};
经典做法是加上前缀:COLOR_BLUE,但是这样会很累赘,有个代替的方法是使用结构体来限定作用域:
1 2 3 4 5 6 7 struct Color1 { enum Type { RED=102 , YELLOW, BLUE }; };
另外两个问题是enum会转换为int,以及表示枚举变量的实际类型不能明确指定,C++11中引入了枚举类来解决:
1 2 3 4 5 6 7 enum class Color2 { RED=2 , YELLOW, BLUE }; Color2 c2 = Color2::RED; cout << static_cast <int >(c2) << endl;
枚举类可以用特定类型来存储enum:
1 2 3 4 5 6 7 8 enum class Color3 :char ; enum class Color3 :char { RED='r' , BLUE }; char c3 = static_cast <char >(Color3::RED);
类中可以使用枚举来表示常量,枚举常量不会占用对象的存储空间,它们在编译时被全部求值。
1 2 3 4 5 6 7 8 class Person {public : typedef enum { BOY = 0 , GIRL }SexType; };
decltype decltype(expression)用于查询表达式的类型(不会对表达式求值)。常见用法:
推导表达式类型:decltype(i) a;
定义类型
重用匿名类型
1 2 3 4 5 struct { int d ; doubel b; }anon_s; decltype (anon_s) as ;
泛型编程中结合auto,追踪函数的返回类型(最大用途):
1 2 3 4 template <typename T>auto multiply (T x, T y) ->decltype (x*y) { return x*y; }
引用和指针 引用与指针
引用
左值是存储在内存中、有明确存储地址(可寻址)的数据,右值是可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。
左值引用是常规引用,一般表示对象的身份。左值引用要求右边的值必须能够取地址,如果不能取址,可以使用指向常量的引用,不过这样数据就无法修改了。
右值引用是必须绑定到一个临时对象、将要销毁的对象的引用,一般表示对象的值。右值引用主要目的是消除不必要的对象拷贝,更简洁明确地定义泛型函数。
1 2 3 4 5 int num = 10 ;int && a = 10 ; a = 100 ; cout << a << endl;
引用型返回值
使用引用型返回值常常是在运算符重载时,例如重载[]:
1 2 vector<int > v (10 ) ;v[5 ] = 10 ;
★ 常量左值引用
常量左值引用既可以绑定左值又可以绑定右值,常见:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <iostream> using namespace std;class Copyable {public : Copyable (){} Copyable (const Copyable &o) { cout << "Copied" << endl; } }; Copyable ReturnRvalue () { return Copyable (); } void AcceptVal (Copyable a) {}void AcceptRef (const Copyable& a) {}int main () { cout << "pass by value: " << endl; AcceptVal (ReturnRvalue ()); cout << "pass by reference: " << endl; AcceptRef (ReturnRvalue ()); }
★ 移动语义
移动语义是为了解决不必要的构造次数过多的问题产生的方法,例如以下的一段MyString类实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #include <iostream> #include <cstring> #include <vector> using namespace std;class MyString { public : static size_t CCtor; public : MyString (const char * cstr=0 ){ if (cstr) { m_data = new char [strlen (cstr)+1 ]; strcpy (m_data, cstr); } else { m_data = new char [1 ]; *m_data = '\0' ; } } MyString (const MyString& str) { CCtor ++; m_data = new char [ strlen (str.m_data) + 1 ]; strcpy (m_data, str.m_data); } MyString& operator =(const MyString& str){ if (this == &str) return *this ; delete [] m_data; m_data = new char [ strlen (str.m_data) + 1 ]; strcpy (m_data, str.m_data); return *this ; } ~MyString () { delete [] m_data; } char * get_c_str () const { return m_data; } private : char * m_data; }; size_t MyString::CCtor = 0 ;int main () { vector<MyString> vecStr; vecStr.reserve (1000 ); for (int i=0 ;i<1000 ;i++){ vecStr.push_back (MyString ("hello" )); } cout << MyString::CCtor << endl; }
在push_back的时候进行了1000次拷贝构造,构造出来的临时字符串都没有什么用,导致了没用的资源申请和释放。如果使用移动语义,就可以直接进行移动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 #include <iostream> #include <cstring> #include <vector> using namespace std;class MyString {public : static size_t CCtor; static size_t MCtor; static size_t CAsgn; static size_t MAsgn; public : MyString (const char * cstr=0 ){ if (cstr) { m_data = new char [strlen (cstr)+1 ]; strcpy (m_data, cstr); } else { m_data = new char [1 ]; *m_data = '\0' ; } } MyString (const MyString& str) { CCtor ++; m_data = new char [ strlen (str.m_data) + 1 ]; strcpy (m_data, str.m_data); } MyString (MyString&& str) noexcept :m_data (str.m_data) { MCtor ++; str.m_data = nullptr ; } MyString& operator =(const MyString& str){ CAsgn ++; if (this == &str) return *this ; delete [] m_data; m_data = new char [ strlen (str.m_data) + 1 ]; strcpy (m_data, str.m_data); return *this ; } MyString& operator =(MyString&& str) noexcept { MAsgn ++; if (this == &str) return *this ; delete [] m_data; m_data = str.m_data; str.m_data = nullptr ; return *this ; } ~MyString () { delete [] m_data; } char * get_c_str () const { return m_data; } private : char * m_data; }; size_t MyString::CCtor = 0 ;size_t MyString::MCtor = 0 ;size_t MyString::CAsgn = 0 ;size_t MyString::MAsgn = 0 ;int main () { vector<MyString> vecStr; vecStr.reserve (1000 ); for (int i=0 ;i<1000 ;i++){ vecStr.push_back (MyString ("hello" )); } cout << "CCtor = " << MyString::CCtor << endl; cout << "MCtor = " << MyString::MCtor << endl; cout << "CAsgn = " << MyString::CAsgn << endl; cout << "MAsgn = " << MyString::MAsgn << endl; }
以上代码直接把右值引用的指针修改了,相当于直接让自己的指针指向右值引向的临时变量的值,这样就避免了一次没有意义的拷贝,而临时变量也马上会被销毁,因此将其指针改为nullpter就可以了。(必须要改,不然数据被释放了)
delete空指针是合法的。并且为了安全,delete非空指针后最好要把指针置空,因为delete只是释放指向的空间,指针还会指向那部分空间。
右值引用可以移动,如果左值引用的局部变量生命周期很短,也是可以移动的,C++11提供了std::move()来将左值转换为右值,告诉编译器使用移动构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 int main () { vector<MyString> vecStr; vecStr.reserve (1000 ); for (int i=0 ;i<1000 ;i++){ MyString tmp ("hello" ) ; vecStr.push_back (tmp); } cout << "CCtor = " << MyString::CCtor << endl; cout << "MCtor = " << MyString::MCtor << endl; cout << "CAsgn = " << MyString::CAsgn << endl; cout << "MAsgn = " << MyString::MAsgn << endl; cout << endl; MyString::CCtor = 0 ; MyString::MCtor = 0 ; MyString::CAsgn = 0 ; MyString::MAsgn = 0 ; vector<MyString> vecStr2; vecStr2.reserve (1000 ); for (int i=0 ;i<1000 ;i++){ MyString tmp ("hello" ) ; vecStr2.push_back (std::move (tmp)); } cout << "CCtor = " << MyString::CCtor << endl; cout << "MCtor = " << MyString::MCtor << endl; cout << "CAsgn = " << MyString::CAsgn << endl; cout << "MAsgn = " << MyString::MAsgn << endl; }
但是需要注意变量作用域,tmp在移动构造以后失去了内容,但是还没有被销毁,这之后使用是可能产生错误的。
通用引用
当遇到模版中的自动类型推导或auto时,&&会变成通用引用,到底是左值还是右值,取决于他的初始化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 template <typename T>void f ( T&& param) {}f (10 ); int x = 10 ; f (x); template <typename T>void f ( T&& param) ; template <typename T>class Test { Test (Test&& rhs); }; void f (Test&& param) ; template <typename T>void f (std::vector<T>&& param) ; template <typename T>void f (const T&& param) ;
完美转发
完美转发是指函数将参数进行转交时,不会改变参数的左/右值特征,就是完美的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void process (int & i) { cout << "process(int&):" << i << endl; } void process (int && i) { cout << "process(int&&):" << i << endl; } void myforward (int && i) { cout << "myforward(int&&):" << i << endl; process (i); } int main () { int a = 0 ; process (a); process (1 ); process (move (a)); myforward (2 ); myforward (move (a)); }
上面的例子是不完美转发,C++11提供了std::forward()函数解决这个问题:
1 2 3 4 5 6 void myforward (int && i) { cout << "myforward(int&&):" << i << endl; process (std::forward<int >(i)); } myforward (2 );
上面修改后,右值可以完美转发了,但是左值还是不行,这时就需要借助上述提到的通用引用来解决:
1 2 3 4 template <typename T>void perfectForward (T && t) { RunCode (forward<T> (t)); }
这样无论是左值还是右值,都可以完美转发了。
二.常见方法 1.RAII RAII的意思是资源获取即初始化,是一种编程技术。RAII保证资源的声明周期与一个对象的生存期相绑定。保证资源能够用于任何访问该对象的函数,避免内存泄露。
RAII可以总结如下:
每个资源封装在一个类中,构造函数请求资源,在无法完成时抛出异常,析构函数释放资源且决不会抛出异常。
在使用资源时始终通过RAII类的具有自动存储器或临时生存期的实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 std::mutex m; void bad () { m.lock (); f (); if (!everything_ok ()) return ; m.unlock (); } void good () { std::lock_guard<std::mutex> lk (m) ; f (); if (!everything_ok ()) return ; }
2.智能指针 参考: 1.https://light-city.github.io
2.https://www.jianshu.com/p/d19fc8447eaa
3.https://zh.cppreference.com/w/cpp/language/raii