目录
前言
- C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的
- 程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
- 1982年,Bjarne Stroustrup 博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
- 同时C++还优化了许多C语言中的不便,并在C语言的基础上增加了许多库、模板、和容器,使其在使用时更加便利与高效
- 现如今,C++已成为全球最受欢迎的编程语言之一,并且对于想要从事服务器开发和游戏开发的同学来说,学好C++十分有必要。
命名空间
🧸在敲第一串C++代码的时候,我们便会发现,在C++中不仅要包含的头文件不一样了还加了一串什么不明所以的东西。
编辑
🧸那我们便从这个东西开始讲:
在C/C++中,变量、函数和类都是大量存在的,自己写的时候可能感受不到,但这些变量、函数和类的名称将都存在于全局作用域中,在一个工程之中可能会导致很多冲突。由此namespace便出现了使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,进而避免不断出现的命名冲突。
命名空间的定义
🧸命名空间 ——— 命名空间域,只影响使用,不影响生命周期。在命名空间外便无法直接访问定义的变量或函数。
namespace Stack { int a = 0; int b; void swap(int* x, int* y) { int tmp = *x; *x = *y; *y = tmp; } }
编辑
🧸同时还可以在一个命名空间之中嵌套另外一个命名空间。同时,更深层的命名空间可以使用较浅层命名空间的变量或函数,而较浅层的命名空间却无法使用深层空间中的内容。
namespace Stack { int a = 0; int b; void swap(int* x, int* y) { int tmp = *x; *x = *y; *y = tmp; } namespace Queue //命名空间的嵌套使用 { int n = 0; int m = 0; void change(int* x,int* y) { swap(x, y); } } }
拓展访问
🧸使用命名空间之后,便无法在全局之中直接访问命名空间定义的数据,若我们想要访问命名空间里定义的数据,可以使用以下三种方法:
- 指定命名空间访问
- 全局展开
- 部分常用展开
指定命名空间访问
🧸使用::(域作用限定符),例如我们要打印出Stack中的a,只需要用 命名空间名称 + :: + 变量名 便可完成对该变量的访问。其实使用::无非就是限制了编译器寻找对象的范围,若不加命名空间名称进行使用则会在全局变量中搜索。
printf("%d ", Stack::a); // Stack(命名空间的名称):: (域作用限定符) a(变量名)
全局展开
🧸全局展开就是我们在开头写的那个,using namespace std,便是全局展开命名空间 std 的内容。全局展开后便可直接访问该命名空间中的数据(若有命名空间嵌套则无法访问深层嵌套的内容)。
namespace Stack { int a = 5; namespace Alpaca { int n = 3; } }; using namespace Stack; int main() { printf("%d ", a); return 0; }
编辑
部分常用展开
🧸在工程之中一般是不会使用全局展开的(容易产生命名重定义的问题),但 std 之中我们常有要使用的函数,比如下面介绍的 cin 和 cout ,每次使用都用域作用限定符来实现未免太过于麻烦,于是我们便可以使用第三种方法,就是只展开我们经常使用的对象,其他的对象仍被限制访问。如此 a 可以被打印而 b 却无法被识别。
编辑
C++的输入输出
- 使用 cout 标准输出对象和cin标准输入对象(键盘)时,必须包含 <iostream> 头文件以及按命名空间使用方法来使用 std 。
- cout 和 cin 是全局的流对象,endl 是特殊的C++符号,表示换行输出。
- 作为C++最常见的输入方式,cin cout 还会自动识别输入数据的类型,并不需要像 printf/scanf 输入输出时那样,需要手动控制格式。
- << 是流插入运算符,>> 是流提取运算符,便于记忆,我们可以这么理解,数据从 cin 来到当前变量之中便是 cin>>n ,当我们要输出时,数据从临时变量流向 cout 进行输出便是 cout<<n 。
编辑
缺省参数
🧸缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。
🧸即有传参则使用传来的参数,若无传参则使用默认的缺省参数运行函数。函数中每个参数都有缺省参数则叫全缺省。
编辑
半缺省
🧸指函数中的参数并非都有缺省参数,但值得注意的是:声明缺省参数时必须从右往左连续,不能跳跃着使用缺省参数。
🧸同时,传参时必须从左往右连续,不可以跳跃着传参。
编辑
注意
🧸缺省参数在声明和定义中不能同时出现,若在声明与定义位置同时出现,并且两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。
推荐在声明之中使用缺省参数。
函数重载
🧸在C语言里,当我们实现一个整型的交换函数之后,若还需要浮点型的交换函数,在调整参数的过程中,还要注意命名不能与上一个交换函数重复。但C++便很好地解决了这个问题。
🧸函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
void swap(int* x, int* y) //整型的交换函数 { int tmp = *x; *x = *y; *y = tmp; } void swap(double* x, double* y) //浮点型的交换函数 { double tmp = *x; *x = *y; *y = tmp; }
函数重载的条件
- 参数类型不同
- 参数个数不同
- 参数类型顺序不同
🧸满足其中的任意一个都能够构成函数重载。
C++为何支持函数重载
🧸在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。为了在链接阶段可以找到对应的函数,每个编译器都有自己的函数名修饰规则,由于Windows下 vs 的修饰规则过于复杂,而 Linux 下 g++ 的修饰规则简单易懂,下面我们使用了 g++ 进行演示。
编辑
编辑
🧸我们可以看出 gcc 的函数修饰后名字不变。而 g++ 的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。C语言没办法支持重载,是因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,在链接时便可区分出不同的函数。
只有返回值不同能否构成重载?
🧸答案是否定的,虽然说直接从定义上看,二者是有区别的,但是当我们调用函数时,编译器就无法判断要调用哪个函数(函数名,参数都相同)。
引用
概念
🧸引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
🧸就好比你叫张三,又有一个小名叫李四,当别人叫李四的时候,你也知道是在叫你。
即二者在计算机中使用的是同一块空间。
特性
引用在定义时必须初始化
编辑
一个变量可以有多个引用
编辑
引用一旦引用一个实体,再不能引用其他实体
🧸引用初始化后便无法对其引用的指向再次修改,若尝试使用复制修改引用的指向,实则会修改原来指向数据的数值。
编辑
常引用
🧸在使用引用的时候,可以加上 const 关键字,对其权限进行限制,之后该引用只能读但不能写。但权限只能缩小不能放大。
🧸就比如原来一个 const 引用的变量,你可以给他赋一个普通引用的值(权限缩小),却无法将其赋给一个普通引用(权限放大)。
编辑
不仅如此,还可以实现对不同类型值的引用。
编辑
🧸程序运行时遇到类型转换都会产生临时变量,使用 ( ) 对变量进行强制类型转换时,也不是直接转换该变量的数据类型,而是产生一个目标类型的临时变量。而临时变量具有常性,若直接使用 double& 进行接收则会出现权限放大的情况,因此需要使用 const bouble& 类型的变量接收。
引用传参
🧸在C语言中,我们运行函数时使用的形参与我们传入的实参并不是同一个东西,形参只是实参的一份临时拷贝。因此形参的改变不会影响实参,所以当我们实现需要对实参进行修改的函数时,就需要传该变量的指针才能影响到实参。例如交换函数,使用了引用之后传入的就是原变量的别名,因此别名的改变会直接改变到原变量的数值,在使用的时候变得更加方便。
void swap(int& x, int& y) { int tmp = x; x = y; y = tmp; }
传引用返回
🧸大家可能不知道,函数的返回值其实也是传了一个临时变量回来,而不是原来的那个变量。不信?看下面的代码。
编辑编辑
🧸可以看到传值回来后会出现权限放大的情况,且使用常引用接收后便无报错,由此可以得知函数传参回来确实是临时变量。同时我们也可以从函数栈帧的角度进行分析,若传回来的值的原来的变量的话,结束函数调用后该函数的栈帧就已经销毁了,即原来的那个变量也不复存在,由此也无法直接传递原来的变量回来。
🧸 无论是 int 还是 double ,我们以前使用的传参返回方式都叫传值返回,而现在使用引用可以跳过产生临时变量这个步骤,为系统节约空间。当前的传参可能体现不出其重要性,若传回来的值是一个复杂的类,若对其再次构造生成一个临时变量进行返回,对系统的消耗是巨大的。由此正确地使用传引用返回十分重要。
传值传参与传引用传参的对比
🧸通过记录时间求出调用两种函数的不同时间,由此可以看出传引用传参的速度是明显快于传值传参的。且随着目标对象复杂程度的增加,二者之间的差距会越拉越远。有兴趣的也可以自己试试这个代码。
编辑
#include<iostream> #include <time.h> using namespace std; struct A { int a[10000]; }; void TestFunc1(A a) {} void TestFunc2(A& a) {} void TestRefAndValue() { A a; // 以值作为函数参数 size_t begin1 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc1(a); size_t end1 = clock(); // 以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc2(a); size_t end2 = clock(); // 分别计算两个函数运行结束后的时间 cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl; } int main() { TestRefAndValue(); }
传值返回与传引用返回的区别
🧸二者的区别相较于传参更加地明显,由此便证明了传引用传参与返回确实能在一定程度上对程序进行优化。
编辑
#include<iostream> #include <time.h> using namespace std; struct A { int a[10000]; }; A a; // 值返回 A TestFunc1() { return a; } // 引用返回 A& TestFunc2() { return a; } void TestReturnByRefOrValue() { // 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(); size_t end1 = clock(); // 以引用作为函数的返回值类型 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc2(); size_t end2 = clock(); // 计算两个函数运算完成之后的时间 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl; } int main() { TestReturnByRefOrValue(); return 0; }
注意
🧸如果函数返回时,出了函数作用域,如果返回对象还没还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
编辑
🧸就像我们平时在使用动态内存申请空间一样,当申请的内存空间被释放后,若我还有该空间的地址,仍然能访问该空间的内容,但是这片空间已经被系统回收了,因此里面有什么数据也是不确定的。
🧸同样的道理也可以用来理解引用,引用就是取别名跟原变量共用一个地址,因此即便函数结束后传回来的是原变量的别名,但该空间已经被系统回收了,由此对该空间访问的结果也是随机的。
🧸所以在使用传引用返回时,需要判断函数结束后被返回值是否还存在,不能够盲目地使用。
与指针的异同
🧸从语法概念上讲,引用实际上就是一个别名,没有独立空间,和其引用实体共用同一块空间。而指针是有独立空间的。
🧸但从底层实现出发,引用其实就是用指针实现的。
🧸通过对汇编代码的查看,我们可以看到实际上引用和指针调用的都是相同的指令。
编辑
区别
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
- 没有 NULL 引用,但有 NULL 指针。
- 在 sizeof 中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小有多级指针,但是没有多级引用。
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
- 引用比指针使用起来相对更安全。
内联函数
概念
以 inline 修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,进而提升程序运行的效率。
实际上是C++对C语言宏定义函数的优化,使这个过程可调试化且会对当前数据类型进行检查。避免了在使用宏时由于优先级的问题而出现的错误。
使用
🧸在函数前加上 inline 便可完成内联函数的定义。但我们发现,此时函数还是用 call 的方式去调用的,而不是直接展开。这是由于在 debug 模式下,需要对编译器进行设置,否则不会展开( debug 模式下,编译器默认不会对代码进行优化)
inline int add(int x, int y) { return x + y; } int main() { int ret = 0; ret = add(2, 5); return 0; }
编辑
🧸如此调整之后,在次通过汇编代码进行查看,此时的汇编代码就与原来不同,实际上便是在原代码之中将内联函数的内容在原函数之中展开。
编辑
编辑
注意
- inline 是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- inline 对于编译器而言只是一个建议,并不是加 inline 就会使用内联。
- 由于内联函数是不进符号表的,所以内联函数在使用时声明与定义不能分离,否则会产生链接错误。
auto
🧸C++11中,标准委员会赋予了 auto 全新的含义即:auto 不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto 声明的变量必须由编译器在编译时期推导而得。
🧸但是使用 auto 定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导 auto 的实际类型。因此 auto 并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将 auto 替换为变量实际的类型。
注意
用auto推导指针
🧸用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别,但用 auto 声明引用类型时则必须加 & 。
编辑
同一行定义多个变量
🧸当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
编辑
auto不能作为函数的参数
🧸auto 不能作为形参类型,因为编译器无法对 a 的实际类型进行推导。
编辑
auto不能直接用来声明数组
编辑
范围for
🧸由于 auto 的自动推导功能,我们可以使用一个全新的循环方式。
🧸对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的 for 循环。for 循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int main() { int arr[] = { 1,2,3,4,5,6 }; for (auto i : arr) { cout << i << ' '; } return 0; }
编辑编辑
🧸但只这样写我们是无法对原数组进行修改的。这是由于 i 只是作为一个与数组值相同的变量,之后我们根据 i 进行输出。
🧸若要对原数组进行修改的话,就需要使用引用,才能进行修改。
编辑编辑
条件
- for循环迭代的范围必须是确定的
- 迭代的对象要实现++和==的操作。
nullptr
🧸在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针都是直接用NULL 进行初始化的。
🧸但实际上 NULL 是一个宏,stddef.h 里是这么定义的。
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
🧸 因此 NULL 可能被定义为字面常量 0 ,或者被定义为无类型指针 (void*) 的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,所以C++引入新的关键字 nullptr 来代替NULL。
一点细节
- 在使用 nullptr 表示指针空值时,不需要包含头文件。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 在后续表示指针空值时建议最好使用 nullptr 。