目录
3.同一个工程中允许存在多个相同名称的命名空间,编译器最后会把它们视为同一个命名空间
前言
什么是C++?
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的
程序,需要高度的抽象和建模时,C语言则不合适。
为了解决软件危机, 20世纪80年代, 计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
一、C++关键字(63个)
——图片源自网络
二、命名空间
为什么C++使用命名空间?
目的:是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字可以应对上述代码存在的问题
2.1 命名空间的定义
1.命名空间中可以定义变量/函数/类型
#include <stdio.h> #include <stdlib.h> namespace myspace { //命名空间中可以定义 变量/函数/类型 int rand = 9; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; }; }
2.命名空间可以嵌套
//命名空间可以嵌套 namespace myspace1 { //命名空间中可以定义 变量/函数/类型 int rand = 9; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; }; // namespace myspace2 { int rand = 10; //使用 myspace1::myspace2::rand } }
3.同一个工程中允许存在多个相同名称的命名空间,编译器最后会把它们视为同一个命名空间
//test.cpp namespace myspace1 { int Add(int left, int right) { return left + right; } } //test.h namespace myspace1 { int Mul(int left, int right) { return left * right; } } ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ namespace myspace1 { int Add(int left, int right) { return left + right; } int Mul(int left, int right) { return left * right; } }
2.2 命名空间的使用
namespace myspace { //命名空间中可以定义 变量/函数/类型 int rand = 9; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; }; } //1.域作用限定符 :: int main() { printf("%d\n", myspace::rand); return 0; } //2.using引用命名空间中的某个成员 using myspace::rand; using myspace::Add; int main() { printf("%d\n", myspace::rand); printf("%d\n", myspace::Add(1,2)); return 0; } //3.using namespace 命名空间名称 引入 using namespace myspace; int main() { printf("%d\n", myspace::rand); printf("%d\n", myspace::Add(1,2)); return 0; }
注意:展开命名空间使用起来更方便,但是风险也更大
三、C++输入/输出
用C++在屏幕上输出一个"hello world"
#include<iostream> // std是C++标准库的命名空间名 using namespace std; int main() { // << 流插入运算符 cout << "hello world" << endl; // c out c-console return 0; }
说明
1. 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件,以及按命名空间使用方法使用std。
2. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在<iostream >头文件中。
3. << 是流插入运算符,>> 是流提取运算符。
4. 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。
C++的输入输出可以自动识别变量类型。但是如果需要控制输出的格式(例如控制浮点数的精度),使用printf就比较方便
#include <iostream> using namespace std; int main() { int i = 0; double j = 0; //自动识别类型 cin >> i >> j; cout << i << endl; cout << j << endl; cout << &i << endl; cout << &j << endl; return 0; }
四、缺省参数
4.1 缺省参数的概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参
#include <iostream> using namespace std; void Func(int a = 1) { cout << a << endl; } int main() { Func(2); Func(); return 0; }
4.2 缺省参数的分类
1.全缺省参数
//全缺省 //缺省值必须是常量或者全局变量 void Func(int a = 10, int b = 20, int c = 30) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; } int main() { Func(); Func(1); Func(1, 2); Func(1, 2, 3); return 0; }
2.半缺省参数(部分缺省参数)
//半缺省(部分缺省) //必须从右往左,给缺省值 //例如:void Func(int a = 10, int b, int c = 30) 是错误的 //因为还没有给出变量b的缺省值就已经给出变量a的缺省值了 void Func(int a, int b = 20, int c = 30) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; } int main() { //Func(); error Func(1); Func(1, 2); Func(1, 2, 3); return 0; }
3.注意:缺省参数不能在函数声明和定义中同时出现
五、函数重载
5.1 函数重载的概念
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
//1.参数类型不同,构成函数重载 int Add(int left, int right) { cout << "int Add(int left, int right)" << endl; return left + right; } double Add(double left, double right) { cout << "double Add(double left, double right)" << endl; return left + right; } int main() { cout << Add(1, 2) << endl; cout << Add(1.11, 2.03) << endl; return 0; } //2.参数个数不同,构成函数重载 void func(int i, double d) { cout << "void func(int i, double d)" << endl; } void func(int i) { cout << "void func(int i)" << endl; } int main() { func(1, 1.1); func(3); return 0; } //3.参数顺序不同,构成函数重载 void func(int i, double d) { cout << "void func(int i, double d)" << endl; } void func(double d, int i) { cout << "void func(double d, int i)" << endl; } int main() { func(1, 1.1); func(1.2, 2); return 0; }
5.2 C++支持函数重载的原理——函数名修饰
C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接;最终生成可执行程序
1.C/C++ 预处理、编译、汇编、链接
test.cpp
预处理:头文件展开/宏替换/去掉注释/条件编译
生成 test.i
编译:检查语法,生成‘汇编代码'(指令级代码) ... ...
生成 test.s
汇编:形成符号表,将‘汇编代码’生成为二进制指令 call func(?地址)
生成 test.o
链接:合并链接,符号表的合并与重定位,生成可执行程序
生成 test.exe注意:每个.c/.cpp文件单独 编译,统一 链接
生成符号表(函数名/变量名与其地址的映射)
1、编译器对每个cpp文件编译生成该文件拥有和需要的符号构成符号表;
2、链接器对多个目标文件形成发布件时,判断所有目标文件中的符号引用都有唯一的定义
(检查符号未定义问题)2.函数名修饰规则
不同的编译器的修饰规则不同
我们可以看到,C++的函数名修饰规则体现出了函数的参数类型,而C语言的函数名没有体现参数的类型。也就是说,函数名相同但是参数类型不同的函数在.cpp的文件中经过处理修饰后的函数名不相同了
六、引用
6.1 引用的概念
引用就是给已存在变量取的一个别名,它们共用同一块内存空间
类型& 引用变量名(对象名) = 引用实体; using namespace std; int main() { int a = 0; int& b = a; // & 引用 cout << &a << endl; // & 取地址 cout << &b << endl; b++; cout << a << endl; // a = 1 } 注意:引用类型必须和引用实体是同种类型的 1. 引用在定义时必须初始化 // int& b; error 2. 一个变量可以有多个引用 // int& b = a; int& c = a; 3. 引用一旦引用一个实体,再不能引用其他实体
6.2 常引用
const -- 常属性 //在引用的过程中,权限可以平移、缩小,但是不能放大 int func() { int a = 0; // ... return a; } int main() { const int a = 0; //权限的放大平移 //int& b = a; //error //int b = a; 这样是可以的,赋值拷贝,b的修改不影响a //权限的平移 const int& c = a; //权限的缩小 int x = 0; const int& y = x; /*int i = 0; double d = i;*/ // (int)i -> (double)临时变量(具有常属性) -> (double)d int i = 0; //double& d = i; //error but why? 权限的放大 const double& d = i; //int& ret = func(); //error 临时变量具有常属性 const int& ret = func(); return 0; } void TestConstRef() { const int a = 1; //int& ra = a; // 该语句编译时会出错,a为常量 // error:将 int& 类型的引用绑定到 const int 类型的初始值设定项时,限定符被丢弃 const int& ra = a; // int& b = 1; // 该语句编译时会出错,b为常量 // error:非常量引用的初始值必须为左值 const int& b = 1; double d = 3.14; //int& rd = d; // 该语句编译时会出错,类型不同 // error:无法用 double 类型的值初始化 int& 类型的引用 const int& rd = d; }
6.3 使用场景
//1.做参数 void Swap(int& p1, int& p2) { int tmp = p1; p1 = p2; p2 = tmp; } int main() { int a = 10; int b = 20; Swap(a, b); cout << a << endl; cout << b << endl; return 0; } //2.做返回值 int& Count() { int sum = 0; for (int i = 1; i <= 10; i++) sum += i; // 传值返回 返回的是sum的拷贝,函数栈帧销毁后变量sum也就不存在了 // 传引用返回 返回的是sum的引用,空间销毁了(还给操作系统了), //仍然可以返回它的引用,但是可能会产生其它问题 return sum; } int main() { //如果这块空间没有被清理,返回的是sum的值;如果被清理了,返回的是随机值 int cnt = Count(); cout << cnt << endl; return 0; } //下面这段代码的运行结果又如何呢? int main() { int& cnt = Count(); // cnt 和 sum 使用的是同一块空间(地址相同 cout << cnt << endl; // 55 cout << cnt << endl; // 随机值 return 0; }
6.4 传值 VS 传引用
分析:以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,需要额外花费时间和空间拷贝数据。以引用作为参数或者返回值类型,在传参和返回期间,函数直接传递实参或者将变量本身直接返回就可以了。
传引用传参(任何情况都可以使用):
1.提高效率
2.输出型参数(形参的修改影响实参)
传引用返回(出了函数的作用域,对象还存在才可以使用):
1.提高效率
2.修改返回对象
6.5 引用和指针
在语法概念上,引用是一个别名,没有独立空间,和其引用实体共用同一块空间
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的
引用和指针的不同点:
1. 引用概念上是一个变量的别名,指针存储一个变量的地址
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6. 引用自增即引用的实体增加1,指针自增即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用由编译器自己处理
9. 引用比指针使用起来相对更安全
七、内联函数
7.1 内联函数的概念
以关键字inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销(类似宏替换),内联函数提升程序运行的效率
一般函数经过反汇编后查看到的编码
以VS2022为例,需要对编译器进行设置才能够查看替换后的编码
内联函数经过反汇编后查看到的编码
7.2 内联函数的特性
1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率
2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建
议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不
是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性,把它当作一般函数处理
3. inline不建议声明和定义分离,如果分离定义和声明在不同的文件内会导致链接错误。因为内联函数直接展开,没有创建函数栈帧、生成函数地址进入符号表等等,链接就会找不到extra 宏的优缺点
缺点
1.导致代码可读性差,可维护性差,容易误用
2.不方便调试宏(因为预编译阶段进行了替换
3.没有类型安全的检查优点
1.增强代码的复用性
2.没有类型的严格限制
3.没有调用的消耗,提高性能
(针对频繁调用的较小的函数,不需要建立函数栈帧,从而提高了效率inline——宏函数,const、enum——宏常量
八、关键字auto(C++11)
关键字auto可以自动推导出需要的类型名称
//普通场景下没有什么意义 int main() { int a = 0; //int b = a; auto b = a; //auto - int auto p = &a; //auto - int * auto& r = a; return 0; } #include <vector> #include <string> //类型名称很长时可以简化代码 int main() { std::vector<std::string> v; //std::vector<std::string>::iterator it = v.begin(); //简化代码 auto it = v.begin(); //auto - std::vector<std::string>::iterator int i = 0; //函数 typeid(变量名).name() 可以获取变量的类型名称 cout << typeid(i).name() << endl; return 0; }
auto不能推导的场景
1. auto不能作为函数的参数 // 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导 void TestAuto(auto a) {} 2. auto不能直接用来声明数组 void TestAuto() { int a[] = {1, 2, 3}; auto b[] = {7, 8, 9}; }
注意:
1.使用auto定义变量时必须对其进行初始化
2.用auto声明指针类型时,用auto和auto*没有任何区别
3.在同一行声明多个变量时,这些变量必须是相同的类型
九、基于范围的for循环(C++11)
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环
//C++98 遍历数组的方式 void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) array[i] *= 2; for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) cout << array[i] << " "; cout << endl; //2 4 6 8 10 } void TestNewFor() { int array[] = { 1, 2, 3, 4, 5 }; /*for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) array[i] *= 2;*/ //for循环后的括号由冒号“ :”分为两部分:第一部分是 //范围内用于迭代的变量,第二部分则表示被迭代的范围 for (auto& x : array) x *= 2; //依次取出数组中的值赋给e(自动判断结束(自动迭代 for (auto e : array) cout << e << " "; cout << endl; //2 4 6 8 10 } int main() { TestFor(); TestNewFor(); return 0; }
注意:
1.与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环
2.for循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围
十、指针空值nullptr(C++11)
我们先看C++98存在的一个大坑
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使nullptr