An's Blog
收藏、分享 …
Toggle navigation
Home
Cesium
SuperMap
ArcGIS
MapboxGL
CentOS
GeoServer
Favorites
Archives
Tags
C++入门基础
2023-08-22 17:18:27
119
0
0
admin
# **一、C++简介** ## **1. 什么是C++** C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代,计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。 1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的序设计。 ## **2. C++的发展史** 1979年,贝尔实验室的本贾尼等人试图分析unix内核的时候,试图将内核模块化于是在C语言的基础上进行扩展,增加了类的机制,完成了一个可以运行的预处理程序,称之为C with classes。 语言的发展也是随着时代的进步,在逐步递进的,让我们来看看C++的历史版本:  ## **## **3. C++中的输入和输出** 新生婴儿会以自己独特的方式向这个崭新的世界打招呼,C++刚出来后,也算是一个新事物,那C++是否也应该向这个美好的世界来声问候呢?我们来看下C++是如何来实现问候的。 #include<iostream> using namespace std; int main() { cout<<"Hello world!!!"<<endl; return 0; } 在C语言中有标准输入输出函数scanf和printf,而在C++中有cin标准输入和cout标准输出。在C语言中使用scanf和printf函数,**需要包含头文件stdio.h。在C++中使用cin和cout,需要包含头文件iostream以及std标准命名空间**。 C++的输入输出方式比C语言更加方便,因为C++的输入输出不需要控制格式,例如:整型为%d,字符型为%c。 #include<iostream> using namespace std; int main() { int a = 1; float b = 2.1; double c= 2.111; char arr[10] = { 0 }; char d[] = "hello world"; cin >> arr; cout << arr << endl; cout << a << endl; cout << b << endl; cout << c << endl; cout << d << endl; return 0; } > 注意:endl,这其中的l不是阿拉伯数字1,而是26个英文字母的l,它的作用相当于换行。 这里我们还要注意下cin的特点,他和C语言中的gets有些像,gets是遇到换行符停止,而cin是以遇到 **空格,tab或者换行符** 作为分隔符的,因此这儿输入hello world会被空格符分隔开来。  这儿我输入的是hello world,但因为输入时出现了空格,所以之后的内容并不会读入,因此arr中存的就是hello。 # **二、C++关键字** C++中总计有63个关键字:  **其中画圈的是C语言的关键字。这里要注意了:false和true并不是C语言的关键字。** ## **1. 字符串字面量** 想象一下如下场景,我们要打印如下内容: this is "test" 我们不得不用如下的代码,对" 进行转义: std::string normal_str = "this is \"test\""; C++11引入了字符串字面量的概念。对于前面的例子,我们就可以通过如下的方式实现我们的目的: std::string normal_str = R"(this is "test")"; C++11支持用户自定义字面量,这里就不再赘述,感兴趣的自行百度。 ## **2. noexcept** C++11新标准引入的noexcept运算符,可以用于指定某个函数不抛出异常。预先知道函数不会抛出异常有助于简化调用该函数的代码,而且编译器确认函数不会抛出异常,它就能执行某些特殊的优化操作。C++ 98/03版本中常用throw()表示,在C++ 11中已经被noexcept代替。 ### **(1)noexcept异常说明** **C++ 98/03版本:** void func(int x) throw(); //不抛出异常 **C++ 11版本:** void func(int x) noexcept; //不抛出异常 void func1(int x); //抛出异常 对于程序违反了异常说明,编译器在编译阶段不会检查报错,但是在程序执行过程中,程序会调用terminate以确保遵守不在运行时抛出异常的承诺。 void func() noexcept { throw exception(); } - 对于一个函数来说,noexcept说明要么出现在该函数的所有生命语句和定义语句中,要么一次也不出现。 - 可以再函数指针的声明和定义中指定noexcept。 - 在typedef和类型别名中不可以出现noexcept。 - 在成员函数中,noexcept需要跟在const以及引用限定符之后,在final、override或虚函数=0之前。 ### **(2)noexcept运算符** noexcept运算符是一个一元运算符,它的返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。 noexcept(f()); //如果f()不抛出异常则结果为true,否则为false noexcept(e); //当e调用的所有函数都做了步抛出说明且e本身不含有throw语句时,表达式为true,否则返回false ## **3. constexpr** 常量表达式,指的就是由多个(≥1)常量组成的表达式。换句话说,如果表达式中的成员都是常量,那么该表达式就是一个常量表达式。这也意味着,常量表达式一旦确定,其值将无法修改。我们知道,C++ 程序的执行过程大致要经历编译、链接、运行这3个阶段。值得一提的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果;而常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。对于用 C++编写的程序,性能往往是永恒的追求。那么在实际开发中,如何才能判定一个表达式是否为常量表达式,进而获得在编译阶段即可执行的“特权”呢?除了人为判定外,C++11 标准还提供有 constexpr 关键字。 constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。 C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。 #include <iostream> using namespace std; int main() { constexpr int num = 1 + 2 + 3; int url[num] = {1,2,3,4,5,6}; couts<< url[1] << endl; return 0; } ## **4. template** - **类模板:**通用的类描述(使用泛型来定义类),进行实例化时,其中的泛型再用具体的类型替换。 - **函数模板:**通用的函数描述(使用泛型来定义函数),进行实例化时,其中的泛型再用具体的类型替换。 ### **(1)C++98标准中两者的区别** 函数模板和类模板在C++98标准中一起被引入,两者区别主要在于 **:在类模板声明时,标准允许其有默认模板参数。而函数模板却不支持** 。默认模板参数的作用如同函数的默认形参。不过在C++11中,这一限制已经被解除了,如下例所示: void DefParm(int m = 3) {} // c++98编译通过,c++11编译通过 template <typename T = int> class DefClass {}; // c++98编译通过,c++11编译通过 template <typename T = int> void DefTempParm() {}; // c++98编译失败,c++11编译通过 可以看到,DefTempParm函数模板拥有一个默认模板参数(类型int)。使用仅支持C++98的编译器编译,DefTempParm的编译会失败,而支持C++11的编译器则无问题。 ### **(2)C++11标准中两者的区别** 尽管C++11支持了函数模板的默认模板参数,不过在语法上,两者还是存在区别: **类模板在为多个默认模板参数声明指定默认值时,必须遵照“从右往左”的规则进行指定。而这个规则对函数模板来说并不是必须的** 。示例如下: template <typename T1, typename T2 = int> class DefClass1 {}; template <typename T1 = int, typename T2> class DefClass2 {}; // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则 template <typename T, int i = 0> class DefClass3 {}; template <int i = 0, typename T> class DefClass4 {}; // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则 template <typename T1 = int, typename T2> void DefFunc1(T1 a, T2 b) {}; // OK 函数模板不用遵循“由右往左”的规则 template <int i = 0, typename T> void DefFunc2(T a) {}; // OK 函数模板不用遵循“由右往左”的规则 可以看到,不按照从右往左定义默认类模板参数的模板类DefClass2和DefClass4都无法通过编译。而对于函数模板来说,默认模板参数的位置则比较随意。DefFunc1和DefFunc2都为第一个模板参数义了默认参数,而第二个模板参数的默认值并没有定义,C++11编译器却认为没有问题。 ## **5. Lambda表达式** lambda源自希腊字母表中第11位的λ,在计算机科学领域,它则是被用来表示一种匿名函数。所谓匿名函数,简单地理解就是没有名称的函数,又常被称为 lambda 函数或者 lambda 表达式。 ### **(1)匿名函数定义** 定义一个 lambda 匿名函数很简单,可以套用如下的语法格式: [外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型 { 函数体; }; - **[外部变量访问方式说明符]**:[ ]方括号用于向编译器表明当前是一个lambda表达式,其不能被省略。**在方括号内部,可以注明当前lambda函数的函数体中可以使用哪些“外部变量”。所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量**。 - **(参数)**:和普通函数的定义一样,lambda匿名函数也可以接收外部传递的多个参数。**和普通函数不同的是,如果不需要传递参数,可以连同()小括号一起省略**; - **mutable**:此关键字可以省略,**如果使用则之前的()小括号将不能省略**(参数个数可以为0)。默认情况下,对于以值传递方式引入的外部变量,不允许在lambda表达式内部修改它们的值(可以理解为这部分变量都是const常量)。而如果想修改它们,就必须使用mutable关键字。 - **noexcept/throw()**:可以省略,**如果使用,在之前的()小括号将不能省略**(参数个数可以为0)。默认情况下,lambda函数的函数体中可以抛出任何类型的异常。而标注 noexcept关键字,则表示函数体内不会抛出任何异常;使用throw()可以指定lambda函数内部可以抛出的异常类型。值得一提的是,如果lambda函数标有noexcept而函数体内抛出了异常,又或者使用throw() 限定了异常类型而函数体内抛出了非指定类型的异常,这些异常无法使用try-catch捕获,会导致程序执行失败。 - **-> 返回值类型**:指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略-> 返回值类型。 - **函数体**:和普通函数一样,lambda匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。**需要注意的是,外部变量会受到以值传递还是以引用传递方式引入的影响,而全局变量则不会。换句话说,在 lambda 表达式内可以使用任意一个全局变量,必要时还可以直接修改它们的值**。 ### **(2)匿名函数中的[外部变量]** 对于 lambda 匿名函数的使用,比较让人感到困惑的就是 [外部变量] 的使用。其实很简单,无非下表所示的这几种编写格式。 | 外部变量格式| 功能 | | :--- | :--- | |[] | 空方括号表示当前lambda 匿名函数中**不导入任何外部变量**。| |[=] | 只有一个 = 等号,表示**以值传递的方式导入所有外部变量**;| |[&] | 只有一个 & 符号,表示**以引用传递的方式导入所有外部变量**;| |[val1,val2,...] | 表示以值传递的方式导入 val1、val2 等指定的外部变量,同时多个变量之间没有先后次序;| |[&val1,&val2,...] | 表示以引用传递的方式导入 val1、val2等指定的外部变量,多个变量之间没有前后次序;| |[val,&val2,...] | 以上 2 种方式还可以混合使用,变量之间没有前后次序。| |[=,&val1,...] | 表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。| |[this] | 表示以值传递的方式导入当前的 this 指针。| > **注意:**单个外部变量不允许以相同的传递方式导入多次。例如 [=,val1] 中,val1 先后被以值传递的方式导入了 2 次,这是非法的。 ### **(3)使用实例** #include <iostream> #include <algorithm> using namespace std; int main() { int num[4] = {4, 2, 3, 1}; //对 a 数组中的元素进行排序 sort(num, num + 4, [=](int x, int y) -> bool{ return x < y; } ); for(int n : num) { cout << n << " "; } return 0; } 程序执行结果为:1 2 3 4。调用sort()函数实现了对num数组中元素的升序排序,其中就用到了lambda匿名函数。而如果使用普通函数,需以如下代码实现: # **三、命名空间** 在C/C++中,**变量、函数和类**都是大量存在的,这些变量、函数和类的名称都将作用于全局作用域中,可能会导致很多命名冲突。 使用命名空间的目的就是对标识符和名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。 ## **1. 命名空间的定义** 定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。 **注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中** ### **(1)命名空间的普通定义** //1. 普通的命名空间,里面可以定义变量,也可以定义函数 namespace AClass { int printf = 1; int rand = 2; int Add(int a, int b) { return a + b; } } ### **(2)命名空间可以嵌套** //2.命名空间可以嵌套 namespace AClass { int printf = 1; int rand = 2; int Add(int a, int b) { return a + b; } namespace BClass { int a = 0; int Sub(int a, int b) { return a - b; } } } ### **(3) 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。** //3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。 namespace AClass { int a = 3; int b = 1; } 它会与上面的AClass命名空间合并 ## **2. 命名空间使用** 下面来看这么一段代码: namespace AClass { int printf = 1; int rand = 2; int Add(int a, int b) { return a + b; } } #include<iostream> int main() { printf("%d\n",printf); //这样打印出来的结果和我们预期的不一样,因为你这样调用的是printf的地址通过下面两个可以加深理解 printf("%p\n", printf); //6A35CE70 printf("%p\n", rand); //6A42FAB0; }  > 很显然直接打印printf是不可能的,因为你这样调用的是printf的地址,所以会出现的这样的结果,正确的调用方法为以下三种。 ### **(1)加命名空间名称及作用域限定符** 符号“::”在C++中叫做作用域限定符,我们通过“命名空间名称::命名空间成员”便可以访问到命名空间中相应的成员  ### **(2)使用using namespace 命名空间名称引入**  > 但是这种方式存在着一些弊端,如果我们在命名空间中定义了一个名字为printf的变量,那么之后再将namespace xjt这个命名空间引入的话,就会造成命名的污染了。  为了解决这个问题,出现了第三种引入方法。 ### **(3)使用using将命名空间中的成员引入**  这种方法可以防止命名的污染,因为它只引入了一部分。 # **四、类型** C++ 03中的基本算术类型包括9种,列举如下:  C++ 11中的基本算术类型包括12种,C++11的基本类型完全包含上述9种类型,除此之外还包括:  C++11标准中的char16_t和char32_t用来处理Unicode字符,char16_t可以作为UTF-16的一个处理单元,char32_t可以作为UTF-32编码的一个处理单元。 ## **1. 枚举类型** ### **(1)传统枚举类型的缺陷** 枚举类型是C/C++中用户自定义的构造类型,它是由用户定义的若干枚举常量的集合。枚举值对应整型数值,默认从0开始。比如定义一个描述性别的枚举类型。 enum Gender{Male,Female}; **其中枚举值Male被编译器默认赋值为0,Female赋值为1。传统枚举类型在设计上会存在以下几个问题:** ① 同作用域同名枚举值会报重定义错误。**传统C++中枚举常量被暴漏在同一层作用域中,如果同一作用域下有两个不同的枚举类型,但含有同名的枚举常量也是会报编译错误的,比如:** enum Fruits{Apple, Tomato, Orange}; enum Vegetables{Cucumber, Tomato, Pepper}; //编译报Tomato重定义错误 > 其中水果和蔬菜两个枚举类型中包含同名的Tomato枚举常量会导致编译错误。因为enum则是非强作用域类型,枚举常量可以直接访问,这种访问方式与C++中具名的namespace、class/struct以及union必须通过"名字::成员名"的访问方式大相径庭。 ② 由于枚举类型被设计为**常量数值的“别名”**,所以枚举常量总是可以被隐式转换为整型,且用户**无法为枚举常量定义类型**。 ③ 枚举常量占用存储空间以及符号性不确定。C++标准规定C++枚举所基于的“基础类型”是由编译器来具体实现,这会导致枚举类型成员的基本类型存在不确定性问题,尤其是符号性问题 。 ### **(2)强类型枚举** 非强作用域类型,允许隐式转换为整型,枚举常量占用存储空间以及符号性的不确定,都是枚举类缺点。针对这些缺点,C++11引入了一种新的枚举类型——强类型枚举(strong-typedenum)。强类型枚举使用enum class语法来声明: enum class Enumeration{VAL1, VAL2, VAL3 = 100, VAL4}; **强类型枚举具有如下几个优点:** ① 强作用域,**强类型枚举成员的名称不会被输出到其父作用域**,所以不同枚举类型定义同名枚举成员编译不会报重定义错误。进而使用枚举类型的枚举成员时,必须指明所属范围,比如Enum::VAL1,而单独的VAL1则不再具有意义; ② 转换限制,**强类型枚举成员的值不可以与整型发生隐式相互转换**。比如比如Enumeration::VAL4==10;会触发编译错误; 可以指定底层类型。强类型枚举默认的底层类型是int,但也可以显示地指定底层类型。具体方法是在枚举名称后面加上":type",**其中type可以是除wchar_t以外的任何整型**。比如: enum class Type:char{Low,Middle,High}; ③ 声明强类型枚举的时候,既可以使用关键字enum class,也可以使用enum struct。事实上,enum struct与enum class在语法上没有任何区别。 ④ 由于强类型枚举是强类型作用域的,故匿名的enum class可能什么都做不了,如下代码会报编译错误: enum class {General, Light, Medium, Heavy} weapon; int main() { weapon = Medium; //编译出错 bool b = weapon == weapon::Medium; //编译出错 return 0; } # **五、参数** ## **1. 缺省参数** 缺省参数是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参。 //缺省参数 #include<iostream> using namespace std; //这儿的0就相当于缺省参数,如果实参什么都没传过来,缺省参数就赋值给a,相当于备胎的意思。 void func(int a = 0) { cout << a << endl; } int main() { func(10); func(); //在c语言中这样写肯定是不行的,但是在c++中有了缺省参数,如果你什么都不传,只要你前面有缺省参数的存在,就能过。 return 0; }  ## **2. 全缺省** 全缺省参数,即函数的全部形参都设置为缺省参数。 //全缺省 #include<iostream> using namespace std; void func(int a = 0, int b = 1, int c = 2) { cout <<"a="<< a << endl; cout << b << endl; cout << c << endl; } int main() { func(); return 0; } ## **3. 半缺省参数** void func(int a, int b, int c = 2) { cout << a << endl; cout << b << endl; cout << c << endl; } ### **(1)半缺省参数必须从右往左依次给出,不能间隔着给。** //错误示例 void func(int a, int b = 2, int c) { cout << a << endl; cout << b << endl; cout << c << endl; } ### **(2)缺省参数不能在函数声明和定义中同时出现** //错误示例 //test.h void func(int a, int b, int c = 3); //test.c void func(int a, int b, int c = 2) { cout << a << endl; cout << b << endl; cout << c << endl; } > **原因:**如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。 ### **(3)缺省值必须是常量或者全局变量。** //正确示例 int x = 3;//全局变量 void func(int a, int b = 2, int c = x) { cout << a << endl; cout << b << endl; cout << c << endl; } # **六、函数** ## **1. 函数重载** **函数重载:** 是函数的一种特殊情况,C++允许在 **同一作用域** 中声明几个功能类似的 **同名函数** ,这些同名函数的 **形参列表(参数个数 或 类型 或 顺序)** 必须不同,常用来处理实现功能类似数据类型不同的问题 #include <iostream> using namespace std; int Add(int x, int y) { return x + y; } double Add(double x, double y) { return x + y; } int main() { cout << Add(0,1) << endl;//打印0+1的结果 cout << Add(1.1,2.2) << endl;//打印1.1+2.2的结果 return 0; } 注意:若仅仅只有返回值不同,其他都相同,则不构成函数重载。 short Add(short left, short right) { return left+right; } int Add(short left, short right) { return left+right; } **函数重载的原理:** 为什么C++支援函数重载,而C语言不可以了? 这里我们就要回顾一下以前的知识了,在运行到执行文件前,要经过:**预编译$\Rightarrow$编译$\Rightarrow$汇编$\Rightarrow$链接**这些阶段其实问题就出在编译完之后的汇编阶段,因为在这里C++和C语言有着些许的不同,下面我们来看看: ### **(1). 采用C语言编译器编译之后**  ### **(2). 采用C++语言编译器编译之后**  ### **(3). 总结** (1) 其实归根到底,还是因为C编译器和C++编译器对函数名的修饰不同。在gcc下的修饰规则是:【_Z+函数长度+函数名+类型首字母】。 (2) 这其实也告诉我们为什么函数的返回类型不同,不会构成函数重载,因为修饰规则并不会受返回值的影响。 ## **2. 函数声明语法** 在C++11中,callable object 包括**传统C函数,C++成员函数,函数对象(实现了()运算符的类的实例),lambda表达式(特殊函数对象**)共4种。程序设计,特别是程序库设计时,经常需要涉及到回调,如果针对每种不同的callable object单独进行声明类型,代码将会非常散乱,也不灵活。如下示例: // 传统C函数 int c_function(int a, int b) { return a + b; } // 函数对象 class Functor { public: int operator()(int a, int b) { return a + b; } }; int main(int argc, char** argv) { int(*f)(int, int); // 声明函数类型,赋值只能是函数指针 f = c_function; cout << f(3, 4) << endl; Functor ff = Functor(); // 声明函数对象类型,赋值只能是函数对象 cout << ff(3, 4) << endl; } 幸运的是,C++标准库的头文件里定义了std::function<>模板,此模板可以容纳所有类型的callable object.示例代码如下: #include <iostream> #include <functional> using namespace std; // 传统C函数 int c_function(int a, int b) { return a + b; } // 函数对象 class Functor { public: int operator()(int a, int b) { return a + b; } }; int main(int argc, char** argv) { // 万能可调用对象类型 std::function<int(int, int)> callableObject; // 可以赋值为传统C函数指针 callableObject = c_function; cout << callableObject(3, 4) << endl; // 可以赋值为函数对象 Functor functor; callableObject = functor; cout << callableObject(3, 4) << endl; // 可以赋值为lambda表达式(特殊函数对象) callableObject = [](int a, int b){ return a + b; }; cout << callableObject(3, 4) << endl; } # **七、extern “C”** 有时候在C++工程中可能需要将某些函数按照C的风格来编译,在函数前加extern “C”,意思是告诉编译器,将该函数按照C语言规则来编译。比如:tcmalloc是google用C++实现的一个项目,他提供tcmallc()和tcfree两个接口来使用,但如果是C项目就没办法使用,那么他就使用extern “C”来解决。 # **八、引用** 引用不是新定义一个变量, **而是给已存在变量取了一个别名** ,编译器不会为引用变量开辟内存空间,它和它引用的变量 **共用同一块内存空间** 。 **类型& 引用变量名(对象名) = 引用实体;** #include<iostream> using namespace std; int main() { int a = 1; int& b = a; //相当于给a起了一个别名为b,int是b的类型 cout << a << endl; cout << b << endl; b = 3; //改变b也就相当于改变了a cout << b << endl; cout << a << endl; }  > 注意:引用类型必须和引用实体是同种类型的 ## **1. 引用的特征** ### **(1)引用在定义时必须初始化** //正确示例 int a = 10; int& b = a;//引用在定义时必须初始化 //错误示例 int a = 10; int &b;//定义时未初始化 b = a; ### **(2)一个变量可以有多个引用** int a = 10; int& b = a; int& c = a; int& d = a; ### **(3)引用一旦引用了一个实体,就不能再引用其他实体** int a = 10; int& b = a; int c = 20; b = c;//你的想法:让b转而引用c  > 但实际的效果,确实将c的值赋值给b,又因为b是a的引用,所以a的值间接变成了20。 ## **2. 常引用** 上面提到,**引用类型必须和引用实体是同种类型的**。但是仅仅是同种类型,还不能保证能够引用成功,这儿我们**还要注意是否可以修改**的问题。 void TestConstRef() { const int a = 10; //int& ra = a; // 该语句编译时会出错,a为常量 const int& ra = a; // int& b = 10; // 该语句编译时会出错,b为常量 const int& b = 10; double d = 12.34; //int& rd = d; // 该语句编译时会出错,类型不同 const int& rd = d; } > 这里的a,b,d都是常量,常量是不可以被修改的,但是如果你用int&ra等这样来引用a的话,那么引用的这个a是可以被修改的,因此会出问题。下面我们来看这么一段代码: #include<iostream> using namespace std; int main() { int a = 10; double&ra = a; } 这个引用对吗?想要弄明白这个问题,首先要明白隐式类型提升的问题,在这里int到double存在隐式类型的提升,而在提升的过程中系统会创建一个常量区来存放a类型提升后的结果。因此到这儿,这段代码一看就是错了,**因为你隐式类型提升时a是存放在常量区中的,常量区是不可以被修改的**,而你用double&ra去引用他,ra这个引用是可以被修改的。 加个const就可以解决这个问题。 #include<iostream> using namespace std; int main() { int a = 10; const double&ra = a; } > 注意:将不可修改的量用可读可写的量来引用是不可以的,但是反过来是可以的,将可读可写的量用只可读的量来引用是可以的。 ## **3. 引用的使用场景** ### **(1)引用做参数** 还记得C语言中的交换函数,学习C语言的时候经常用交换函数来说明传值和传址的区别。现在我们学习了引用,可以不用指针作为形参了。因为在这里a和b是传入实参的引用,我们将a和b的值交换,就相当于将传入的两个实参交换了。 //交换函数 void Swap(int& a, int& b) { int tmp = a; a = b; b = tmp; } ### **(2)引用做返回值** 当然引用也能做返回值,但是要特别注意,**我们返回的数据不能是函数内部创建的普通局部变量**,因为在函数内部定义的普通的局部变量会随着函数调用的结束而被销毁。我们返回的数据必须是被static修饰或者是动态开辟的或者是全局变量等不会随着函数调用的结束而被销毁的数据。 **不加static的后果:**  你是不是疑惑为什么打印的不是2而是7了?  这人就更奇怪了,为什么中间加了一句printf,就打印随机值了? 下面我们来看看分析:  > **为什么会出现随机值,因为你在函数里定义的变量是临时变量,出了函数函数是会销毁的,这时它就随机指向内存中的一块空间了**。所以在引用做函数返回值时最好还是给在函数中定义的变量加上static。这时你觉得你真的懂这段代码了吗? #include<iostream> using namespace std; int& Add(int a, int b) { static int c = a + b; return c; } int main() { int& ans = Add(1,2); Add(3, 4); cout << ans << endl; }  可能你会好奇了?为什么这儿是3了?下面来看看分析  其实你换种写法,这儿的结果就会换成7,原因也很简单,正是上面图片中说的原因  > 注意:如果函数返回时,出了函数作用域,返回对象还未还给系统,则可以使用引用返回;如果已经还给系统了,则必须使用传值返回。这句话说的是下面这种例子: ## **4. 引用和指针的区别** 在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。 int main() { int a = 10; int& ra = a; cout<<"&a = "<<&a<<endl; cout<<"&ra = "<<&ra<<endl; return 0; }  **在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。** int main() { int a = 10; int& ra = a; ra = 20; int* pa = &a; *pa = 20; return 0; } 我们来看下引用和指针的汇编代码对比  > **引用和指针的区别 :** > ① 引用在定义时必须初始化,指针没有要求。 > ② 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。 > ③ 没有NULL引用,但有NULL指针。 > ④ 在sizeof中的含义不同:引用的结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。 > ⑤ 引用进行自增操作就相当于实体增加1,而指针进行自增操作是指针向后偏移一个类型的大小。 > ⑥ 有多级指针,但是没有多级引用。 > ⑦ 访问实体的方式不同,指针需要显示解引用,而引用是编译器自己处理。 > ⑧ 引用比指针使用起来相对更安全。 # **九、内联函数** ## **1. 概念** 以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。(看到在加粗部分时,小伙伴肯定会想,这和c语言中的宏是不是很像了?)  如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用  ## **2. 特性** (1) inline是一种以**空间换时间**的做法,省去调用函数额开销。所以**代码很长/递归的函数不适宜使用作为内联函数**。 (2) inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内代码比较长/递归等等,编译器优化时会忽略掉内联。 (3) inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。 //F.h #include <iostream> using namespace std; inline void f(int i); // F.cpp #include "F.h" void f(int i) { cout << i << endl; } // main.cpp #include "F.h" int main() { f(10); return 0; } // 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用 ## **3. c++有哪些技术可以代替宏** > 常量定义 换用const > 函数定义 换用内联函数 # **十、auto关键字(C++11)** 在早期的C/C++中auto的含义是:使用auto修饰的变量是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它。 在C++11中,标准委员会赋予了auto全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。 可能光看这一句话,你不一定能懂,下面我们举几个例子。 #include<iostream> using namespace std; int TestAuto() { return 10; } int main() { int a = 10; auto b = a; auto c = 'a'; auto d = TestAuto(); cout << typeid(b).name() << endl; //这个地方要学到后面类的时候才可以解释,这里打印出的是类型名 cout << typeid(c).name() << endl; cout << typeid(d).name() << endl; cout << a << endl; cout << b<< endl; cout << c << endl; cout << d << endl; //auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化 return 0; }  > **注意:** 使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。 ## **1. auto的使用细则** ### **(1)auto与指针和引用结合起来使用** 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加& #include <iostream> using namespace std; int main() { int a = 10; auto b = &a; //自动推导出b的类型为int* auto* c = &a; //自动推导出c的类型为int* auto& d = a; //自动推导出d的类型为int //打印变量b,c,d的类型 cout << typeid(b).name() << endl;//打印结果为int* cout << typeid(c).name() << endl;//打印结果为int* cout << typeid(d).name() << endl;//打印结果为int return 0; } > **注意:** 用auto声明引用时必须加&,否则创建的只是与实体类型相同的普通变量,只不过将其换了个姓名而已。 ### **(2)在同一行定义多个变量** 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。 void TestAuto() { auto a = 1, b = 2; auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同 } ## **2. auto的限制** ### **(1)使用 auto 的时候必须对变量进行初始化** ### **(2)auto不能在函数的参数中使用** // 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导 void TestAuto(auto a) { } > 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。 ### **(3)auto不能作用于类的非静态成员变量** ### **(4)auto关键字不能定义数组,如比如下面的例子就是错误的:** char url[] = "http://c.biancheng.net/"; auto arr[] = url; //arr 为数组,所以不能使用 auto ### **(5)auto不能作用于模板参数** ## **3. decltype** decltype 是 C++ 11新增的一个关键字,它和 auto 的功能一样,都用来在编译时期进行自动类型推导。既然已经有了 auto 关键字,为什么还需要 decltype 关键字呢?因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来非常不方便,甚至压根无法使用,所以 decltype 关键字也被引入到 C++11 中。auto 和 decltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的: auto varname = value; decltype(exp) varname = value; 其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。auto 根据=右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。另外,**auto要求变量必须初始化,而decltype不要求**。这很容易理解,auto是根据变量的初始值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。decltype 可以写成下面的形式: decltype(exp) varname; 原则上讲,exp 就是一个普通的表达式,它可以是任意复杂的形式,但是我们必须要保证 exp 的结果是有类型的,不能是 void;例如,当 exp 调用一个返回值类型为 void 的函数时,exp 的结果也是 void 类型,此时就会导致编译错误。C++ decltype 用法举例: int a = 0; decltype(a) b = 1; //b 被推导成了 int decltype(10.8) x = 5.5; //x 被推导成了 double decltype(x + 100) y; //y 被推导成了 double # **十一、右值引用** ## **1. 左值和右值** 在 C++ 或者 C 语言中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式。确切的说 C++ 中左值和右值的概念是从 C 语言继承过来的。左值的英文简写为“lvalue”,右值的英文简写为“rvalue”。很多人认为它们分别是"left value"、"right value" 的缩写,其实不然。lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 "read value",指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。 通常情况下,判断某个表达式是左值还是右值,最常用的有以下 2 种方法: - 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。 - 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。 ## **2. 右值引用** C++98/03 标准中就有引用,使用 "&" 表示。但此种引用方式有一个缺陷,即正常情况下只能操作 C++ 中的左值,无法对右值添加引用。举个例子: int num = 10; int &b = num; //正确 int &c = 10; //错误 如上所示,编译器允许我们为num左值建立一个引用,但不可以为10这个右值建立引用。因此,C++98/03标准中的引用又称为**左值引用**。为此,C++11标准新引入了另一种引用方式,称为**右值引用,用"&&"表示**。和声明左值引用一样,**右值引用也必须立即进行初始化操作,且只能使用右值进行初始化**,比如: int num = 10; int && a = num; // error,右值引用不能初始化为左值 int && a = 10; ## **3. 移动构造函数** 在 C++ 11 标准之前(C++ 98/03 标准中),如果想用其它对象初始化一个同类的新对象,只能借助类中的复制(拷贝)构造函数。拷贝构造函数的实现原理很简单,就是为新对象复制一份和其它对象一模一样的数据。 #include <iostream> using namespace std; class demo{ public: demo():num(new int(0)) { cout<<"construct!"<<endl; } //拷贝构造函数 demo(const demo &d):num(new int(*d.num)) { cout<<"copy construct!"<<endl; } ~demo() { cout<<"class destruct!"<<endl; } private: int *num; }; demo get_demo() { return demo(); } int main(){ demo a = get_demo(); return 0; } 可以看到,程序中定义了一个可返回 demo 对象的 get_demo() 函数,用于在 main() 主函数中初始化 a 对象,其整个初始化的流程包含以下几个阶段: 1)执行 get_demo() 函数内部的 demo() 语句,即调用 demo 类的默认构造函数生成一个匿名对象; 2)执行 return demo() 语句,会调用拷贝构造函数复制一份之前生成的匿名对象,并将其作为 get_demo() 函数的返回值(函数体执行完毕之前,匿名对象会被析构销毁); 3)执行 a = get_demo() 语句,再调用一次拷贝构造函数,将之前拷贝得到的临时对象复制给 a(此行代码执行完毕,get_demo() 函数返回的对象会被析构); 4)程序执行结束前,会自行调用 demo 类的析构函数销毁 a。 完整的输出结果如下: onstruct! <-- 执行 demo() copy construct! <-- 执行 return demo() class destruct! <-- 销毁 demo() 产生的匿名对象 copy construct! <-- 执行 a = get_demo() class destruct! <-- 销毁 get_demo() 返回的临时对象 class destruct! <-- 销毁 a 如上所示,利用拷贝构造函数实现对a对象的初始化,底层实际上进行了2次拷贝(而且是深拷贝)操作。当然,对于仅申请少量堆空间的临时对象来说,深拷贝的执行效率依旧可以接受,但如果临时对象中的指针成员申请了大量的堆空间,那么2次深拷贝操作势必会影响 a 对象初始化的执行效率。 所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。以前面程序中的demo类为例,该类的成员都包含一个整形的指针成员,其默认指向的是容纳一个整形变量的堆空间。当使用get_demo() 函数返回的临时对象初始化 a 时,我们只需要将临时对象的 num 指针直接浅拷贝给a.num,然后修改该临时对象中num指针的指向(通常另其指向NULL),这样就完成了a.num的初始化。 #include <iostream> using namespace std; class demo{ public: demo():num(new int(0)) { cout<<"construct!"<<endl; } demo(const demo &d):num(new int(*d.num)) { cout<<"copy construct!"<<endl; } //添加移动构造函数 demo(demo &&d):num(d.num) { d.num = NULL; cout<<"move construct!"<<endl; } ~demo(){ cout<<"class destruct!"<<endl; } private: int *num; }; demo get_demo() { return demo(); } int main() { demo a = get_demo(); return 0; } 可以看到,在之前 demo 类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,**此构造函数使用右值引用形式的参数,又称为移动构造函数**。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。 命令执行此程序,输出结果为: construct! move construct! class destruct! move construct! class destruct! class destruct! 通过执行结果我们不难得知,当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。我们知道,非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。 ## **4. move语义** C++11标准中**借助右值引用可以为指定类添加移动构造函数,这样当使用该类的右值对象(可以理解为临时对象)初始化同类对象时,编译器会优先选择移动构造函数**。注意,移动构造函数的调用时机是:用同类的右值对象初始化新对象。那么,用当前类的左值对象(有名称,能获取其存储地址的实例对象)初始化同类对象时,是否就无法调用移动构造函数了呢?当然不是,C++11 标准中已经给出了解决方案,即调用 move() 函数。move 本意为 "移动",但该函数并不能移动任何数据,它的功能很简单,就是将某个左值强制转化为右值。move() 函数的用法也很简单,其语法格式如下: move( arg ) ## **5. 完美转发** 完美转发指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。举个例子: template<typename T> void function(T t) { otherdef(t); } 如上所示,function() 函数模板中调用了 otherdef() 函数。在此基础上,完美转发指的是:如果 function() 函数接收到的参数 t 为左值,那么该函数传递给 otherdef() 的参数 t 也是左值;反之如果 function() 函数接收到的参数 t 为右值,那么传递给 otherdef() 函数的参数 t 也必须为右值。显然,function() 函数模板并没有实现完美转发。一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;另一方面,无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。总之,无论从那个角度看,function() 函数的定义都不“完美”。 C++11 标准中规定, **通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)** 。 #include <iostream> using namespace std; //重载被调用函数,查看完美转发的效果 void otherdef(int & t) { cout << "lvalue\n"; } void otherdef(const int & t) { cout << "rvalue\n"; } //实现完美转发的函数模板 template <typename T> void function(T&& t) { otherdef(forward<T>(t)); } int main() { function(5); int x = 1; function(x); return 0; } 程序执行结果为: rvalue lvalue # **十二、基于范围的for循环(C++11)** ## **1. 范围for的语法** 在C++98中如果要遍历一个数组,可以按照以下方式进行: void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; //将数组所有元素乘以2 for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) array[i] *= 2; for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p) cout << *p << endl; } > 对于一个有范围的集合而言,对程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。注意不能写成auto,不然改变不了原数组  **正确的写法:** void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; //将数组中所有元素乘以2 for(auto& e : array) e *= 2; for(auto e : array) cout << e << " "; return 0; } > **注意:** 与普通循环类似,可用continue来结束本次循环,也可以用break来跳出整个循环。 ## **2. 范围for的使用条件** ### **(1)for循环迭代的范围必须是确定的** 对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。 注意:以下代码就有问题,因为for的范围不确定 void TestFor(int array[]) { for(auto& e : array) //这里的array其实不是数组,数组在传参时会退化成指针 cout<< e <<endl; } ### **(2)迭代的对象要实现++和==的操作** 关于迭代器这个问题,以后会讲,现在大家了解一下就可以了。 # **十三、指针** ## **1. 指针简介** ### **(1) C++98中的指针空值** 在良好的C/C++编程习惯中,在声明一个变量的同时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误。比如未初始化的指针,如果一个指针没有合法的指向,我们基本都是按如下方式对其进行初始化: int* p1 = NULL; int* p2 = 0; NULL其实是一个宏,在传统的C头文件(stddef.h)中可以看到如下代码: #ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif > 可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如: #include <iostream> using namespace std; void Fun(int p) { cout << "Fun(int)" << endl; } void Fun(int* p) { cout << "Fun(int*)" << endl; } int main() { Fun(0); //打印结果为 Fun(int) Fun(NULL); //打印结果为 Fun(int) Fun((int*)NULL); //打印结果为 Fun(int*) return 0; } > 程序本意本意是想通过Fun(NULL)调用指针版本的Fun(int* p)函数,但是由于NULL被定义为0,Fun(NULL)最终调用的是Fun(int p)函数。 > 注:在C++98中字面常量0,既可以是一个整型数字,也可以是无类型的指针(void*)常量,但编译器默认情况下将其看成是一个整型常量,如果要将其按照指针方式来使用,必须对其进行强制转换。 ### **(2) C++11中的指针空值** > 对于C++98中的问题,C++11引入了关键字nullptr。在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为关键字引入的。在C++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同,大小都为4。 ## **2. 智能指针** 所谓智能指针,可以从字面上理解为“智能”的指针。具体来讲,智能指针和普通指针的用法是相似的,不同之处在于,**智能指针可以在适当时机自动释放分配的内存**。也就是说,使用智能指针可以很好地避免“忘记释放内存而导致内存泄漏”问题出现。由此可见,C++也逐渐开始支持垃圾回收机制了,尽管目前支持程度还有限。C++98/03标准中,支持使用auto_ptr智能指针来实现堆内存的自动回收;C++11新标准在废弃auto_ptr的同时,增添了unique_ptr、shared_ptr以及weak_ptr这3个智能指针来实现堆内存的自动回收。 ### **(1) shared_ptr** 和unique_ptr、weak_ptr不同之处在于,**多个shared_ptr智能指针可以共同使用同一块堆内存。并且,由于该类型智能指针在实现上采用的是引用计数机制,即便有一个 shared_ptr 指针放弃了堆内存的“使用权”(引用计数减 1),也不会影响其他指向同一堆内存的 shared_ptr 指针**(只有引用计数为 0 时,堆内存才会被自动释放)。shared_ptr<T> 类模板中,提供了多种实用的构造函数: std::shared_ptr<int> p1; //不传入任何实参 std::shared_ptr<int> p2(nullptr); //传入空指针 nullptr std::shared_ptr<int> p3(new int(10)); // 在构建 shared_ptr 智能指针,也可以明确其指向。 std::shared_ptr<int> p3 = std::make_shared<int>(10); // C++11 标准中还提供了 std::make_shared<T> 模板函数 // 调用拷贝构造函数 std::shared_ptr<int> p4(p3); std::shared_ptr<int> p4 = p3; // 调用移动构造函数 std::shared_ptr<int> p5(std::move(p4)); std::shared_ptr<int> p5 = std::move(p4); 同一普通指针不能同时为多个 shared_ptr 对象赋值,否则会导致程序发生异常。例如: int* ptr = new int; std::shared_ptr<int> p1(ptr); std::shared_ptr<int> p2(ptr); // 错误 ### **(2) unique_ptr** 作为智能指针的一种,unique_ptr指针自然也具备“在适当时机自动释放堆内存空间”的能力。**和shared_ptr指针最大的不同之处在于,unique_ptr指针指向的堆内存无法同其它 unique_ptr 共享,也就是说,每个unique_ptr指针都独自拥有对其所指堆内存空间的所有权。这也就意味着,每个unique_ptr指针指向的堆内存空间的引用计数,都只能为1,一旦该unique_ptr指针放弃对所指堆内存空间的所有权,则该空间会被立即释放回收**。 std::unique_ptr<int> p1(); // 创建出空的 unique_ptr 指针: std::unique_ptr<int> p2(nullptr); // 创建出空的 unique_ptr 指针: std::unique_ptr<int> p3(new int); // 创建出了一个 p3 智能指针,其指向的是可容纳 1 个整数的堆存储空间。 // 基于 unique_ptr 类型指针不共享各自拥有的堆内存,因此 C++11 标准中的 unique_ptr 模板类没有提供拷贝构造函数,只提供了移动构造函数 std::unique_ptr<int> p4(new int); std::unique_ptr<int> p5(p4);// 错误,堆内存不共享 std::unique_ptr<int> p5(std::move(p4)); // 正确,调用移动构造函数 ### **(2) weak_ptr** 需要注意的是,C++11标准虽然将weak_ptr定位为智能指针的一种,但该类型指针通常不单独使用(没有实际用处),只能和shared_ptr类型指针搭配使用。甚至于,我们可以将weak_ptr类型指针视为shared_ptr指针的一种辅助工具,**借助weak_ptr类型指针,我们可以获取shared_ptr指针的一些状态信息,比如有多少指向相同的shared_ptr指针、shared_ptr指针指向的堆内存是否已经被释放等等**。 需要注意的是,当 weak_ptr 类型指针的指向和某一shared_ptr指针相同时,weak_ptr指针并不会使所指堆内存的引用计数加1;同样,当weak_ptr指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。也就是说,weak_ptr 类型指针并不会影响所指堆内存空间的引用计数。除此之外,weak_ptr<T>模板类中没有重载*和->运算符, **这也就意味着,weak_ptr类型指针只能访问所指的堆内存,而无法修改它** 。 std::weak_ptr<int> wp1; // 可以创建一个空 weak_ptr 指针 std::weak_ptr<int> wp2 (wp1); // 凭借已有的 weak_ptr 指针,可以创建一个新的 weak_ptr 指针 // 利用已有的 shared_ptr 指针为其初始化 std::shared_ptr<int> sp (new int); std::weak_ptr<int> wp3 (sp); # **十四、多线程Thread** ## **1. 基础概念** C++11新标准中引入五个头文件支持多线程编程,他们分别是:thread atomic mutex condition_variable future ① **thread头文件**:该头文件主要声明了 std::thread类,另外std::this_thread命名空间也在改头文件中。 ② **atomic头文件**:该头文件主要声明了std::atomic和std::atomic_flag两个类,另外还申明了一套C风格的原子类型与C兼容的原子操作的函数。 ③ **mutex头文件**:该头文件主要声明了与互斥量(mutex)相关的类,包括std::mutex系列类、std::lock_guard类std::unique_lock类等。 ④ **condition_variable头文件**:该头文件主要声明了与条件变量相关的类,包括 std::condition_variable和std::condition_variable_any两个类。 ⑤ **future头文件**:该头文件主要声明了: > Futures类:std::future, shared_future > Providers类:std::promise, > std::package_task Providers函数:std::async() # **十五、C++标准库(STD)** ## **1. 元组std::tuple** **std::tuple**是类似pair的模板。每个pair的成员类型都不相同,但每个pair都恰好有两个成员。std::tuple:成员类型不同,有任意数量的成员。使用方式如下: (1)tuple的创建 创建一个空的元组, 创建时,需要指定元组的数据类型 std::tuple<int, float, double, long, long long> first; 创建一个元组并初始化元组 std::string str_second_1("_1"); std::string str_second_2("_2"); // 指定了元素类型为引用 和 std::string, 下面两种方式都是可以的,只不过第二个参数不同而已 std::tuple<std::string, std::string> second_1(str_second_1, std::string("_2")); std::tuple<std::string, std::string> second_2(str_second_1, str_second_2); 使用make_tuple创建元组 int i_fourth_1 = 4; int i_fourth_2 = 44; // 下面的两种方式都可以 std::tuple<int, int> forth_1 = std::make_tuple(i_fourth_1, i_fourth_2); auto forth_2 = std::make_tuple(i_fourth_1, i_fourth_2); (2)tuple的遍历 #include <iostream> // std::cout #include <tuple> // std::tuple, std::tuple_size int main () { std::tuple<int, char, double> mytuple (10, 'a', 3.14); // tuple的大小 std::cout << "mytuple has "; std::cout << std::tuple_size<decltype(mytuple)>::value; std::cout << " elements." << '\n'; // 获取tuple的元素类型 std::tuple_element<0, decltype(mytuple)>::type ages; // ages就为int类型 // 获取元素 std::cout << "the elements is: "; std::cout << std::get<0>(mytuple) << " "; std::cout << std::get<1>(mytuple) << " "; std::cout << std::get<2>(mytuple) << " "; std::cout << '\n'; return 0; } //输出结果: mytuple has 3 elements. the elements is: 10 a 3.14
Pre:
C++标准库(STL)
Next:
倾斜摄影三维模型OSGB格式转换3DTILES的关键技术浅析
0
likes
119
Weibo
Wechat
Tencent Weibo
QQ Zone
RenRen
Table of content