C++ 入门
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
C++是在C的基础之上容纳进去了面向对象编程思想并增加了许多有用的库以及编程范式等
文章目录
C++ 为了解决 C语言中存在的一些不好的地方从而增加了一些语法
一、命名空间
在项目组中进行代码合并时可能会遇到自己和其他人写的代码定义了相同的名字发生命名冲突在C语言中只能在产生错误后修改名字没有很好的预防手段
自己定义的变量名函数名类型名等都可能会和别人定义的名字冲突或者和库的名字冲突因为此时相同的名字处在同一个域(全局域)
而在不同的域定义了相同的名字并不会产生命名冲突使用时是局部优先
C++ 中增加了另一种新的域叫做命名空间用 namespace 关键字定义
命名空间定义
namespace 空间名
{
//类型变量函数等
} //花括号后没有分号
将处在全局域中的变量函数类型等封装在命名空间域中便可以防止命名冲突
在命名空间域中也包含全局域和局部域因此不会影响变量的生命周期
在命名空间域中内部对内部的访问也是遵守先声明后使用优先使用局部然后使用命名空间域中的全局最后使用命名空间域外的全局
命名空间域会影响编译时期的查找规则在查找一个变量、函数、类型是否定义时默认是在局部找(代码块内)局部不存在便会去全局找全局找不到便会报错不会直接去已经定义的命名空间中找
外部想要访问命名空间内部的内容时有三种方法
- 指定命名空间访问使用域作用限定符 :: (两个冒号)
- 展开命名空间中的所有内容using namespace 命名空间名
- 展开命名空间中的常用内容using 命名空间名 :: 内容
C++ 中将标准库函数的代码放在 iostream 头文件中并用 std 命名空间封装防止用户和库中的命名冲突这里以 cout 为例
//指定命名空间访问
//:: 域作用限定符左边为空代表在全局域中查找
#include <iostream>
int main()
{
std::cout << "hello world";
return 0;
}
//展开命名空间中的所有内容
#include <iostream>
using namespace std;
int main()
{
cout << "hello world";
return 0;
}
//展开命名空间中的常用内容
#include <iostream>
using std::cout;
int main()
{
cout << "hello world";
return 0;
}
平时练习时可以使用全部展开写项目时建议展开常用的和指定访问
还需要注意几点
- 命名空间的名字相同时会合并为一个命名空间但是名字相同的命名空间中如果存在相同的定义会报错
- 命名空间可以定义在命名空间中即命名空间可以嵌套定义
二、输入输出
C++ 中标准库提供的 cin 可以用于输入cout 可以用于输出cin 和 cout 自动识别类型不用指定输入输出格式cout 中 endl 表示换行
#include <iostream>
//为了可以找到封装在 std 中的 cin 和 cout
using namespace std;
int main()
{
char c;
int i;
double d;
//自动识别类型cin >> 可以简单理解为数据从控制台流向变量
cin >> c >> i >> d;
//自动识别类型输出数据后换行cout << 可以简单理解为数据从变量流向控制台
cout << c << endl;
cout << i << endl;
cout << d << endl;
cout << "hello C++" << endl;
return 0;
}
三、缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个缺省值在调用该函数时如果省略指定实参则采用该形参的缺省值否则使用指定的实参。
声明或定义函数时只能从右往左连续的为函数的参数指定缺省值调用函数传参时也只能从右往左连续省略实参
#include <iostream>
using namespace std;
//只能从右往左连续的为函数的参数指定缺省值
//void f1(int a, int b = 3, int c) -- 错误写法
void f1(int a, int b = 3, int c = 10)
{
cout << a << " ";
cout << b << " ";
cout << c << endl;
}
int main()
{
//只能从右往左连续省略实参
//f1(1, , 3); -- 错误写法
f1(1);
f1(1, 2);
f1(1, 2, 3);
return 0;
}
注意
- 有缺省参数时声明和定义不能同时给缺省参数推荐在声明给如果出现声明和定义分离(多文件时)只能在声明给定义时不给
- 缺省值只能是常量或全局变量(一般不使用全局变量)
//报错非法将局部变量作为缺省参数
void f1(int a, int b = a)
{
cout << a << endl;
}
四、函数重载
C++ 允许 在同一作用域中 声明几个功能类似的 同名函数但是需要满足这些同名函数的形参列表(参数个数 或 参数类型 或 参数类型顺序)不同常用来处理实现功能类似但参数的数据类型不同的问题
#include <iostream>
using namespace std;
//参数个数不同
void f1()
{
cout << "f1()" << endl;
}
void f1(int a)
{
cout << "f1(int a)" << endl;
}
//参数类型不同
int Add(int A, int B)
{
return A + B;
}
double Add(double A, double B)
{
return A + B;
}
//类型顺序不同
void f2(int i, char c)
{
cout << "f2(int i, char c)" << endl;
}
void f2(char c, int i)
{
cout << "f2(char c, int i)" << endl;
}
int main()
{
//参数个数不同
f1();
f1(1);
//参数类型不同
cout << Add(1, 2) << endl;
cout << Add(1.1, 2.2) << endl;
//参数顺序不同
f2(1, 'a');
f2('a', 1);
return 0;
}
未重载的函数调用函数传参类型不同时会隐式转换
重载的函数调用函数时实参的参数列表 和 所有重载的函数的参数列表 都不匹配时不会隐式转换编译器无法分辨调用哪个函数便会报错
C++ 函数重载的原理 – 对函数名修饰
以 Linux 下的 gcc/g++函数名修饰规则为例
用 gcc 编译 .c 文件后用 objdump -S 可执行程序 指令查看函数名修饰规则
用 g++ 编译 .cpp 文件后用 objdump -S 可执行程序 指令查看函数名修饰规则
在编译时期对函数名进行修饰之后便可以根据调用函数时给的参数类型确定函数调用的是哪个重载函数因此重载函数只会影响编译速度
函数调用时返回值可以选择性的使用因此只有返回值不同的同名函数调用时无法区分便不能构成重载
五、引用
1. 引用的用法
//定义引用
//类型& 引用名 = 引用实体;
int a = 10;
int& ra = a;
在使用上引用不是定义新变量而是给指定的变量取别名和该变量共用同一块空间
#include <iostream>
using namespace std;
int main()
{
//定义变量 i
int i = 0;
//定义新变量 j, i 和 j 占用不同的空间
int j = i;
//给变量 i 取别名, i 和 k 占用相同的空间
int& k = i;
//i 和 j 的地址不同
//i 和 k 的地址相同
cout << &i << " ";
cout << &j << " ";
cout << &k << endl;
++i;
++j;
++k;
//结果为 2 1 2
cout << i << " ";
cout << j << " ";
cout << k << endl;
return 0;
}
注意
- 引用是变量的别名
- 引用在定义时必须初始化
- 变量可以有多个引用可以给引用初始化为变量的别名(给别名取别名都是同一个变量)
- 引用指定实体后不能在引用其他实体
- 没有 NULL 引用
- 在 sizeof 中引用结果为引用类型大小
- 没有多级引用引用指针引用数组可以按以下内容理解
引用不需要开辟新空间没有引用的引用
引用不需要开辟新空间没有引用的指针
引用不需要开辟新空间不能作为数组元素(数组元素有空间)即不存在引用数组
2. 常引用
C++ 中指针和引用在使用时权限只能保持或缩小但是 权限不能放大
先以指针为例
#include <iostream>
using namespace std;
int main()
{
//指针权限放大 - error
//i 是常变量不可以被修改
//int* 可以通过解引用修改变量的值
//不可以修改 --> 可以修改 权限放大(不允许)
const int i = 0;
int* pi = &i; //报错const int* 无法转换为 int*
//指针权限保持
//i 是常变量不可以被修改
//const 修饰 *pi因此 pi 不能通过解引用修改变量 i 的值
//不可以修改 --> 不可以修改 权限保持
const int i = 0;
const int* pi = &i;
//指针权限缩小
//i 是变量可以被修改
//const 修饰 *pi因此 pi 不能通过解引用修改变量 i 的值
//可以修改 --> 不可以修改 权限缩小
int i = 0;
const int* pi = &i;
return 0;
}
引用也是如此
#include <iostream>
using namespace std;
int main()
{
//引用权限放大 - error
const int i = 0;
int& ri = i; //报错const int 无法转换为 int&
int& r = 10; //报错
//引用权限保持
const int i = 0;
const int& ri = i;
const int& r = 10;
//引用权限缩小
int i = 0;
const int& ri = i;
return 0;
}
3. 引用的使用场景
(1) 输出型参数
#include <iostream>
using namespace std;
//引用做参数可以直接修改实参的值
void Swap(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
int main()
{
int a = 10, b = 20;
cout << a << " " << b << endl;
//直接传变量 a 和 b;
Swap(a, b);
cout << a << " " << b << endl;
return 0;
}
输出
10 20
20 10
(2) 返回值引用
在函数栈帧的知识中函数的返回值是通过临时变量返回的(为什么不能直接返回因为函数调用结束回到调用函数处时被调用函数的栈帧已经还给操作系统了)如果返回值较小则使用寄存器如果返回值较大则在调用函数的栈帧中开辟
下述代码中变量 n 在静态区当函数调用结束回到调用函数的栈帧中时变量 n 不会被销毁但是编译器还是会通过临时变量的方式带回返回值
int Count()
{
static int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
return 0;
}
此时程序员可以自行调整代码将返回值设置为引用便不会使用临时变量而是使用原空间的别名
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
return 0;
}
注意不要返回当前函数栈帧中的变量的引用
#include <iostream>
using namespace std;
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
//ret 的结果是随机值
cout << "Add(1, 2) is " << ret << endl;
return 0;
}
什么情况下可以返回引用调用函数结束回到调用函数处时变量不会销毁的都可以做为引用返回
如在堆上开辟的空间静态区参数中的引用等
引用做返回值时’ 可以对函数的返回值进行修改 '在之后学习的运算符重载中作用很大
传引用明显的比传值效率高
参数传值时形参是实参的临时拷贝返回值传值时使用的是临时变量
参数传引用时形参是实参的别名返回值传引用时使用的是变量的别名
4. 引用的底层实现
实现上引用是由指针实现的也是需要开辟空间的虽然实现上是这样但是为了便于使用引用还是以使用上的概念为主
int main()
{
int a = 0;
//实现上将 a 的地址赋给指针 ra
int& ra = a;
//实现上编译器默认将 ra 解释为(*ra)
//因此&ra 解释为 &(*ra) == &a 所以 &a 和 &ra 的地址一样
ra = 10;
int* pa = &a;
*pa = 10;
return 0;
}
//启动调式右键进入反汇编
int a = 0;
00007FF71DA317AD mov dword ptr [a],0
//引用和指针的指令在汇编层面一模一样
int& ra = a;
00007FF71DA317B4 lea rax,[a]
00007FF71DA317B8 mov qword ptr [ra],rax
ra = 10;
00007FF71DA317BC mov rax,qword ptr [ra]
00007FF71DA317C0 mov dword ptr [rax],0Ah
int* pa = &a;
00007FF71DA317C6 lea rax,[a]
00007FF71DA317CA mov qword ptr [pa],rax
*pa = 10;
00007FF71DA317CE mov rax,qword ptr [pa]
00007FF71DA317D2 mov dword ptr [rax],0Ah
引用类型必须和引用实体是同种类型的不同类型无法直接赋值但可以通过以下两种方式
在 C/C++ 中显示/隐式类型转换是通过临时变量来完成的临时变量具有常属性(不可以被修改)
#include <iostream>
using namespace std;
int main()
{
int a = 2;
double& ra1 = (double&)a;
const double& ra2 = a;
//输出 2 -9.25596e+61 2
cout << a << " " << ra1 << " " << ra2 << endl;
return 0;
}
//启动调式右键进入反汇编
int a = 2;
00007FF7780D23CD mov dword ptr [a],2
//mov 将 2 放到 a 中
double& ra1 = (double&)a;
00007FF7780D23D4 lea rax,[a]
00007FF7780D23D8 mov qword ptr [ra1],rax
//lea 取出 a 的地址放到 rax
//mov 将 rax 中的内容放到指针 ra1 中
const double& ra2 = a;
00007FF7780D23DC cvtsi2sd xmm0,dword ptr [a]
00007FF7780D23E1 movsd mmword ptr [rbp+68h],xmm0
00007FF7780D23E6 lea rax,[rbp+68h]
00007FF7780D23EA mov qword ptr [ra2],rax
//cvtsi2sd 取出 a 中低 64 位将其转换为浮点数放到 xmm0
//movsd 将 xmm0 放到临时变量 rdb+68h(h 表示十六进制) 空间中
//lea 取出 rdb+68h 空间的地址放到 rax
//mov 将 rax 中的内容放到指针 ra2 中
引用 ra1 是 a 的别名因此 ra1 会以浮点数的方式看待 a 中的值(这里可以修改 ra1存在问题待以后解决)
引用 ra2 是赋值时隐式转换过程中 临时变量的别名因此 ra2 是以浮点数的方式看待浮点类型的临时变量的值
引用比指针使用起来更安全
六、内联函数
在 C语言中宏的缺点1. 不能调试 2. 没有类型检查 3. 有些场景下非常复杂容易出错宏的优点1. 速度快(不用创建函数栈帧) 2. 类型不固定
在 C++ 中为了弥补宏的缺点推荐 用 const 和 enum 代替 宏常量用 内联函数 代替 宏函数
用 inline 修饰的函数称作内联函数(可以调试有类型检查书写和函数一样容易记忆)编译时 C++ 编译器会 在调用内联函数的地方展开(速度快)
为了可以在 debug 模式观察到内联函数的展开需要对编译器进行设置(因为 debug 模式下编译器默认不会对代码进行优化)
在 vs 2022 中
右击项目 -> 属性 -> C/C++ 下的常规 -> 调式信息格式 改为 程序数据库 (/Zi)
右击项目 -> 属性 -> C/C++ 下的优化 -> 内联函数扩展 改为 只适用于 __inline (/Ob1)
#include <iostream>
using namespace std;
//内联函数
inline void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10, b = 20;
Swap(a, b);
cout << a << " " << endl;
return 0;
}
//启动调式右键进入反汇编
//汇编指令中函数调用时需要通过 call 指令跳转到函数地址
//Swap 调用时的汇编指令中没有 call也就说明内联函数展开了
Swap(a, b);
00007FF64AB41552 mov eax,dword ptr [a]
00007FF64AB41556 mov dword ptr [rsp+60h],eax
00007FF64AB4155A mov eax,dword ptr [b]
00007FF64AB4155E mov dword ptr [a],eax
00007FF64AB41562 mov eax,dword ptr [rsp+60h]
00007FF64AB41566 mov dword ptr [b],eax
内联函数特性
- 函数用 inline 修饰对编译器而言只是一个建议在编译阶段是否会展开取决于编译器不加 inline 修饰的函数不会展开
建议 将函数规模小不是递归并且频繁调用的函数 用 inline 修饰否则编译器会忽略 inline 特性 - 内联函数是一种以空间换时间的方法
优点少了函数调用开销提升效
缺点可执行程序变大 - 内联函数如果声明和定义分离会导致链接错误
编译时会将 F.h 中的声明拷贝到 test.cpp 中此时 test.cpp 中只有内联函数 f 的声明没有内联函数 f 的定义于是便无法将 f 函数展开(.cpp 文件是单独编译的)并且由于内联函数会展开所以内联函数 f 并不会在 test.cpp 的符号表中产生也就导致在链接时合并符号表后无法找到函数 f
书写内联函数时在 .h 文件中 定义
七、auto 关键字(C++11)
在早期的 C/C++ 中 auto 用来表示变量具有自动存储器的局部变量(但是一直没有人使用 auto)
到了 C++11标准委员会赋予了 auto 全新的含义auto 不再是一个存储类型指示符而是一个新的类型指示符(为了避免与 C++98 中的 auto 发生混淆C++11 只保留了 auto 作为类型指示符的用法)
auto 声明的变量由编译器在编译时期 根据初始化的值的类型 自动推到变量的类型
#include <iostream>
using namespace std;
int main()
{
//auto 声明的变量必须初识化
//auto a; --error
//auto 自动推导变量类型
auto c = 'a';
auto i = 10;
auto d = 3.14;
//auto 声明指针时 auto 和 auto* 没有区别但 auto* 一定要初识化为地址
auto p1 = &i;
auto* p2 = &i;
//引用类型必须用 auto&
auto& r = i;
//typeid 是操作符typeid(变量).name() 获取变量的类型
//输出char int double int * __ptr64 int * __ptr64 int
cout << typeid(c).name() << " ";
cout << typeid(i).name() << " ";
cout << typeid(d).name() << " ";
cout << typeid(p1).name() << " ";
cout << typeid(p2).name() << " ";
cout << typeid(r).name() << endl;
return 0;
}
auto 不是一种 “类型” 的声明而是一种类型声明时的 “占位符”编译器在编译期会将 auto 替换为变量实际的类型所以 auto 声明多个变量时编译器只会对第一个变量的类型进行推导然后将 auto 替换为推导的类型因此每个变量的类型应该相同(不相同会编译错误)
auto 不能作为函数的参数因为编译时无法知道实参的类型
auto 不能用来声明数组
在以后的学习中某些类型较长容易写错此时便可以使用 auto 关键字自动推导这种情况下也可以使用 typedef 来解决不过 typedef 在某些情况存在一些问题
typedef char* pstr;
int main()
{
//编译报错const 修饰 p1需要指定初始化
//const pstr p1;
//const 修饰 p2编译通过
const pstr* p2;
return 0;
}
八、范围 for – 语法糖(C++11)
C++11 中引入了基于范围的 for 循环
范围 for 循环后的括号由冒号 : 分为两部分
第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围。
#include <iostream>
using namespace std;
int main()
{
int array[] = { 1, 2, 3, 4, 5, 6 };
//自动将数组 array 中的元素依次赋值给 i自动判断结束
//i 是循环变量auto 是循环变量的类型
for (auto i : array)
{
cout << i << " ";
}
cout << endl;
return 0;
}
注意和普通循环一样可以使用 continue 和 break
九、nullptr(C++11)
在定义指针变量时如果不知道指针的指向通常会将指针变量指向 NULL
在 C++98 中NULL实际是一个宏在传统的 C 头文件 stddef.h 中
//在 C++ 中NULL 被定义成 0
//在 C 语言中NULL 被定义成 ((void *)0)
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
由于在 C++98 中 NULL 被定义为了 0在使用 NULL 时难免会遇到一些麻烦
#include <iostream>
#include <stddef.h>
using namespace std;
void f(int a)
{
cout << "f(int)" << endl;
}
void f(int* p)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
return 0;
}
输出:
f(int)
f(int)
程序本意是想通过 f(NULL) 调用指针版本的 f(int*) 函数但是由于 NULL 被定义成 0因此与程序的初衷相悖
C++11 中引入了新的关键字 nullptr表示指针空值((void*)0)sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同