前言
此篇,就正式进入C++的学习了。
C++,高级语言,从名字也能窥见一二,是C语言的 “升级版”。它对C语言中许多空白的地方进行填补,不足的地方进行优化,允许我们 “面向对象编程”。因此我们在学习过程中可以多多对比C++和C。它的优势也和C一样,可以直接操控硬件,相比其他高级语言较底层,因此适合做游戏开发、服务器开发等等偏后端的事儿。
今天是C++学习的第一站,我们先来学学基础语法第一课——C++入门
主要内容有:
- 命名空间
- 缺省参数
- 函数重载
- 引用
- 内联函数
- auto
- 范围for
- nullptr
1. 命名空间
1.1 C的命名缺陷
C语言中关于命名,同个域中不允许变量、类型同名,也不允许函数同名。这会带来一些问题
- 自己定义的变量、函数,可能会和库里的冲突
- 做大项目的时候,也常出现命名冲突问题
#include <stdio.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
:10
现在可以正常输出,那我这样呢?
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}

此处就是自己定义的变量和库里的冲突,说到这,我们再问一个问题:
问:编译器是怎么找变量的?
#include <stdio.h>
int rand = 10;
int main()
{
int rand = 20;
printf("%d\n", rand);
return 0;
}
:20
局部 ==> 全局 ==> 找不到则报错
C++为了解决这个问题,提出了命名空间的概念…
1.2 命名空间
namespace 命名空间是C++内置的关键字
命名空间的定义
先看看它用起来是什么样的
#include <stdio.h>
namespace bacon
{
int pig = 10;
}
int main()
{
printf("%d\n", bacon::pig);
}
:10
语法:namespace [命名空间名]
有点像结构体啊,是的,结构体和命名空间的本质都是封装,只不过结构体封装的是类型,命名空间封装的是“名字”,它的本质是:
改变编译器查找规则?代码里的 : : 是什么东西?
命名空间的使用
域作用限定符
语法:mynamespace : : numbers
? 命名空间 : : 成员
上面的代码中, bacon : : pig 的含义就是,在命名空间bacon中查找pig成员,这也是“改变编译器查找规则”的意思
编译器查找规则
命名空间的使用分以下三种:
-
使用时指定成员 printf("%d", bacon :: pig);
:10
隔离效果最佳,但是使用较麻烦 -
引入指定成员 using bacon :: pig;
printf("%d", pig);
:10
常用的可以引入 -
引入整个命名空间 using namespace bacon;
printf("%d ", pig);
printf("%s", song);
:10 Mojito
隔离失效,平时练习可以这么用,做项目之类的就别了
命名空间的嵌套定义
namespace bacon
{
int pig = 10;
namespace idol
{
namespace jay
{
const char* song = "Mojito";
}
}
}
int main()
{
printf("%s\n", bacon::idol::jay::song);
}
:Mojito
2. hello world
了解完命名空间,我们进一步学习 hello world 程序
#include <iostream>
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
:hello world
问:那想控制输出格式呢?
用printf方便就用呗!哪个方便用哪个
3. 缺省参数(默认参数)
缺省参数,就是在声明函数的某个参数的时候,为之指定一个默认值,在调用该函数的时候如果采用该默认值,你就无须指定该参数
缺省参数也分 全缺省 和 半缺省
-
全缺省 #include <iostream>
using namespace std;
int f1(int e1 = 10, int e2 = 20)
{
return e1 + e2;
}
int main()
{
int ret = f1();
cout << ret << endl;
return 0;
}
:30
-
半缺省:只能从右往左连续缺省(需要缺省的往后放) int f2(int e1, int e2 = 20)
{
return e1 + e2;
}
int main()
{
int ret = f2(100);
cout << ret << endl;
return 0;
}
:120
注意:
4. 函数重载
函数重载(fuction overload),指 我们可以通过传不同的参数来区分同名的函数。
简单来说,“一词多义”,一个函数名能调用执行不同的功能;反过来说,不同功能的函数可以同名。只需要:参数列表不同(类型、个数、顺序)。简单归纳:
构成函数重载:
#include <iostream>
using namespace std;
int Add(int e1, int e2)
{
return e1 + e2;
}
double Add(double e1, double e2)
{
return e1 + e2;
}
int main()
{
int i1 = 10;
int i2 = 20;
double d1 = 1.1;
double d2 = 2.2;
int reti = Add(i1, i2);
double retd = Add(d1, d2);
cout << reti << endl;
cout << retd << endl;
return 0;
}
:30
3.3
cin/cout 的自动识别类型也是函数重载,通过参数列表区分不同的输出类型

函数重载的二义性
现有两个构成重载的函数,调用时我们传的参数无法让编译器明确区分两个函数,我们称这两个构成重载的函数有 二义性
#include <iostream>
using namespace std;
void f()
{
cout << "f()" << endl;
}
void f(int e1 = 0, int e2 = 0)
{
cout << "f(int e1 = 0, int e2 = 0)" << endl;
}
int main()
{
return 0;
}
不调用并编译,能编译成功,说明构成重载
现在来调用一下看看
#include <iostream>
using namespace std;
void f()
{
cout << "f()" << endl;
}
void f(int e1 = 0, int e2 = 0)
{
cout << "f(int e1 = 0, int e2 = 0)" << endl;
}
int main()
{
f();
return 0;
}

实际使用中要注意避免产生二义性
函数重载的简单原理
函数重载的原理不适合现阶段解剖,我们浅浅了解一下就够
我们知道,函数调用就是找到函数名对应的地址,执行地址处的函数体,但,相同的函数名怎么找到不同的地址?
虽然C++中构成函数重载的函数名看起来一样,但其实不尽相同…
函数名修饰
我们先来回忆一下 依函数声明找函数定义 的过程,
- Add.c:声明Add函数
- test.c:调用Add函数
程序从源文件到可执行程序:
源文件 ==预处理==> ==编译==> ==汇编==> ==链接==> 可执行程序
- 预处理:展开头文件,替换宏,过滤注释…
- 编译:不同源文件隔离编译,期间进行 语法分析、词法分析、语义分析、符号汇总
- 汇编:生成 .o 目标文件 、形成符号表
- 链接:合并段表、合并符号表、符号表重定位
*段表:把逻辑段映射到物理内存区

*符号表:编译过程时,源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址
再来看:test.c 中调用了 Add,但编译器在编译后链接前,要找函数定义的时候,发现 test.o 的目标文件内没有函数地址(找不到函数定义),就到 Add.o(其他目标文件) 中找,找到后链接(找不到就是链接错误)
接下来再看C和C++中,找函数定义时的区别:
(由于win的vs下函数名修饰太复杂,Linux下的gcc/g++就很好理解,所以下面用gcc演示)
int Add(int a, int b)
{
return a + b;
}
void func(int a, double b, int* p)
{}
int main()
{
Add(1, 2);
func(1, 2, 0);
return 0;
}


可以看到,C编译器编译后,函数名不变;C++编译器编译后函数名被修饰了:[ _Z + 函数名长度 + 函数名 + 参数类型首字母 ]
我们使两个函数构成重载时,参数列表的不同,已经决定了两个函数是“不一样的函数”了,怎么说?
不同的参数列表,使函数名被修饰成不同的样子,有了修饰后 根本意义上不同的函数名,这样一来,C++编译器通过函数名修饰规则,就能找到对应代码
问:返回值类型不同能不能构成函数重载?
不能的话,那我们在函数名修饰中带上返回值类型不就好了吗?
:不能构成,而且也并不是因为函数名修饰,而是 调用时的二义性——
调用的地方不能控制返回值的类型,我调用返回值为int的,你给我调用返回值为char的,那可不行
归纳一下,函数重载:
- 是参数列表不同的同名函数,能执行不同代码
- 返回值必须相同
- 参数列表不能有二义性
- 如 全缺省 和 无参数 的函数构成重载,调用时就会有二义性
5. 引用
引用,一种数据类型,像是变量的同位语,或是给它取别名,一块空间的不同名字
比如: 周杰伦,JayChou,亚洲音乐天王…
周杰伦就是JayChou,JayChou就是亚洲音乐天王,没有区别
#include <iostream>
using namespace std;
int main()
{
int i = 10;
int& ri = i;
int& rri = i;
cout << &i << endl;
cout << &ri << endl;
cout << &rri << endl;
return 0;
}
:02D8FBE0
02D8FBE0
02D8FBE0
特性:
- 一个实体可以有多个引用
- 引用必须初始化,且初始化后不能更改引用的实体
#include <iostream>
using namespace std;
int main()
{
int i1 = 10;
int i2 = 20;
int& ri = i1;
ri = i2;
cout << ri << endl;
return 0;
}
:20
5.1 引用的应用场景
作参数
#include <iostream>
using namespace std;
int f(int& ri)
{
return ri *= 10;
}
int main()
{
int i = 10;
int ret = f(i);
cout << ret << endl;
return 0;
}
ri 接收到参数 i 后就成了 i 的别名,对 ri 操作 == 对 i 操作
引用作参数的优势:
- 减少拷贝,提高效率
- 引用参数是输出型参数——函数中修改形参,实参也跟着改变
作返回值
int f1()
{
int n1 = 1;
return n1;
}
int& f2()
{
int n2 = 2;
return n2;
}
先回忆一下 传值返回…
传值返回
:创建一个和返回值类型同类型的临时变量(之后称作tmp)==> 将 n1 放进 tmp ==> 销毁栈帧 ==> 通过临时变量带回返回值
这样一来就有一层 拷贝
int tmp = n1;
return tmp;
(tmp小就创建在寄存器内,大就创建在上一层栈帧)
需要十分注意的是:临时变量具有常性!!
问:不能直接返回n1吗?
n1是开辟在栈上的临时变量,出作用域就销毁,数据不由我们控制了
引用返回
:相比传值返回,唯一区别就是临时变量的类型是引用
像代码中写到的,也就是把返回值放到一个int&类型的临时变量,返回这个临时变量(n2 的别名)
int& tmp = n2;
return tmp;
显然危险—— n2已经销毁,可得:
实体返回后仍存在,则可以安全返回它的引用;反之不行
引用返回的优势:
- 减少拷贝, 提高效率
- 可以对返回值操作
5.2 const 引用
const引用,常引用,引用的实体为常量(具有常性)的引用
要学const引用,首先得知道这一原则:
指针和引用的赋值中 权限可以缩小或平移,不能放大
为什么就是指针和引用?它们都影响实体
int main()
{
int a = 10;
int& ra = a;
const int& cra = a;
const int b = 20;
int& rb = b;
const int& crb = b;
return 0;
}
现在再看一个传值返回的例子:
#include <iostream>
using namespace std;
int f()
{
int n = 10;
return n;
}
int main()
{
int& ret = f();
return 0;
}

要求给int&的初始值必须是可以修改,难道不是吗?
其实
都会产生临时变量
而,最最重要的还是,这个临时变量具有常性,不可修改!
#include <iostream>
using namespace std;
int f()
{
int n = 10;
return n;
}
int main()
{
//类型转换,产生临时变量
double d = 10;
//int& rd = (int)d;//error
const int& rd = (int)d;//ok
//传值返回,产生临时变量
//int& ret = f();//error
const int& ret = f();//ok
return 0;
}
哦哦哦,懂了,然后呢,有什么用?
问:为了减少拷贝提高效率,把函数的参数都设计成引用,好么?
不好,传参会受限制,可能会权限放大——比如形参是可读可写的引用,实参传了只读的实体
那怎么做?
const引用不就来了嘛,形参设计成const引用,你不管传什么,都是 权限缩小或平移
#include <iostream>
using namespace std;
int f(const int& e1)
{
//...
}
int main()
{
int a = 10;//缩小
const int b = 20;//平移
f(a);
f(b);
return 0;
}
5.3 引用的实现
既然它和指针这么像,我们就好好看看它们到底有什么区别,上反汇编…

lea(load effective address):加载有效地址
mov:类似赋值操作
二者都是
- 把 [i] 的有效地址 加载到寄存器 eax 中
- 把 eax 中的地址赋给 [ra]/[pa]
可知,引用底层实现上是占空间的,也可知,指针和引用的底层实现是一样的。但是语法上,引用还是不占空间
6. 内联函数
C语言中,对于规模小、结构简单、重复调用的小函数,总是开辟栈帧开辟栈帧,很浪费性能,C++提出内联函数来解决这个问题
6.1 内联 inline
inline是C++中的关键字,只是建议编译器将某函数视为内联函数,编译器会根据情况来决定
特性:
- 内联函数在预处理后会在调用函数的地方直接展开,不会开辟栈帧,而是直接执行指令
*也代表如果内联函数的规模大/结构复杂,会造成代码膨胀:比如递归,疯狂地展开一堆代码,最直接的体现就是最终出来的exe文件会变得很大
应用:小函数
本质上,inline是一种空间换时间的做法
面试题
问:宏的优缺点?
优点:
缺点:
- 可读性差,可维护性差,易错
- 没有类型安全的检查
- 无法调试
问:如何解决宏的缺点?
- 换用const enum来定义常量
- inline 小函数
7. auto(C++11)
auto,一种数据类型,可以根据被赋的值自动推导类型,是C++的关键字
为什么会出现这样的数据类型?学到后面,会发现类型的命名简直复杂:
#include <string>
#include <map>
int main()
{
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange","橙子" },{"pear","梨"} };
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
}
return 0;
}
std::map<std::string, std::string>::iterator 是一个类型,也许可以typedef?
typedef char* pstring;
int main()
{
const pstring p1;
const pstring* p2;
return 0;
}
typedef要求我们 声明变量的时候必须知道之后会给它赋什么类型的值,有时候很苦恼, 那看看auto
std::map<std::string, std::string>::iterator it = m.begin();
auto it = m.begin
舒服
int main()
{
int x = 1;
auto a1 = x;
auto a2 = &x;
auto* a3 = &x;
auto& a4 = x;
cout << typeid(a1).name() << endl;
cout << typeid(a2).name() << endl;
cout << typeid(a3).name() << endl;
cout << typeid(a4).name() << endl;
return 0;
}
:int
int *
int *
int
使用注意事项
- auto声明引用类型时,必须指定auto为引用类型(否则会直接识别为原类型)
int main()
{
int x = 1;
auto a1 = &x;
auto* a2 = &x;
auto a3 = x;
auto& a4 = x;
cout << typeid(a1).name() << endl;
cout << typeid(a2).name() << endl;
cout << typeid(a3).name() << endl;
cout << typeid(a4).name() << endl;
*a1 = 10;
cout << x << endl;
a3 = 20;
cout << x << endl;
a4 = 30;
cout << x << endl;
return 0;
}
- 一行声明多个auto变量时,整行的变量类型必须相同(auto根据第一个变量类型,确定后续的类型)
int main()
{
auto a1 = 1234, a2 = 123.4;
return 0;
}

- auto不能作为函数参数
函数开辟栈帧前,要先根据参数计算要开辟多大空间,但是auto大小不确定使得编译器无法计算
void TestAuto(auto a)
{}

- auto不能直接声明数组
int main()
{
auto a[] = { 1, 2, 3, 4 };
return 0;
}

- 为了和C++98的auto混淆,C++11的auto只作为类型指示符
8. 基于范围的 for
范围for,自动判断范围,自动迭代
使用时,需要给一个迭代变量,再给定迭代范围,每次迭代都重新创建迭代变量
- 迭代变量:可以直接用auto,方便
- 迭代范围:必须是确定的
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (auto e : arr)
cout << e << ' ';
cout << endl;
return 0;
}
:1 2 3 4 5 6 7 8 9 10
使用注意事项
-
迭代时,编译器每次取范围内的数据,赋值给迭代变量,迭代变量并不改变范围内数据 int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (auto e : arr)
e *= 2;
for (auto e : arr)
cout << e << ' ';
cout << endl;
return 0;
}
:1 2 3 4 5 6 7 8 9 10
如果想改变,设计成 auto& e int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (auto& e : arr)
e *= 2;
for (auto e : arr)
cout << e << ' ';
cout << endl;
return 0;
}
:2 4 6 8 10 12 14 16 18 20
用auto*可以吗?不行,范围for只是每次取 arr[0]、arr[1],是int类型,不能用int*接收 -
迭代范围必须是确定的
- 如果迭代数组,范围就是第一个元素到最后一个元素
- 如果迭代类,要提供begin( )、 end( ) 两个方法
void TestRangeFor(int[] arr)
{
for (auto e : arr)
cout << e << endl;
}
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
TestRangeFor(arr);
return 0;
}
-
迭代的对象要实现++和==的操作。(现在做个了解,以后才讲的请)
9. nullptr
学习了这么长时间,多多少少知道NULL本质上是标识符,但是它在传统C的头文件(stddef.h )中的定义是这样:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++中,它的定义出现了bug,被定义成了字面常量0,这也导致许多地方的使用出现问题:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
:f(int)
f(int)
f(int*)
我们希望NULL是void* 类型的指针空值,但却被识别成字面常量int类型的0,想正常用还要强转,很麻烦。这么简单的bug,C++委员会怎么不修复?
语言要向前兼容,有些代码就按照这个特性跑得好好的,你修复,我代码崩了,我还能好受嘛。所以只能打补丁:
用 nullptr 来代替 NULL 的功能
nullptr 就相当于 (void*)0
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(nullptr);
return 0;
}
:f(int)
f(int*)
注意:
- nullptr 是关键字
- C++11中,sizeof(nullptr) 和 sizeof((void*)0) 相等
- 后续最好使用 nullptr
今天的分享就到这里,感谢大家能看到这,不足之处多多交流
这里是培根的blog,期待与你共同进步! For(int[] arr) { for (auto e : arr) cout << e << endl; }
int main() { int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
TestRangeFor(arr);
return 0;
}
3. 迭代的对象要实现++和==的操作。(现在做个了解,以后才讲的请)
# 9. nullptr
学习了这么长时间,多多少少知道NULL本质上是标识符,但是它在传统C的头文件(stddef.h )中的定义是这样:
```cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++中,它的定义出现了bug,被定义成了字面常量0,这也导致许多地方的使用出现问题:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
:f(int)
f(int)
f(int*)
我们希望NULL是void* 类型的指针空值,但却被识别成字面常量int类型的0,想正常用还要强转,很麻烦。这么简单的bug,C++委员会怎么不修复?
语言要向前兼容,有些代码就按照这个特性跑得好好的,你修复,我代码崩了,我还能好受嘛。所以只能打补丁:
用 nullptr 来代替 NULL 的功能
nullptr 就相当于 (void*)0
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(nullptr);
return 0;
}
:f(int)
f(int*)
注意:
- nullptr 是关键字
- C++11中,sizeof(nullptr) 和 sizeof((void*)0) 相等
- 后续最好使用 nullptr
今天的分享就到这里,感谢大家能看到这,不足之处多多交流
这里是培根的blog,期待与你共同进步!
|