C++是从C中生长出来的,主要是解决C语言的不足~Java自立门户,然后疯狂抄C++的作业,但是Java很优秀,Java一些语法比C++还好,C#疯狂抄Java的作业 ~
本贾尼在贝尔实验室用C语言研究Unix内核,感觉C怎么用怎么不舒服,于是改出了C++
1.命名空间
1.1 命名空间的概念
同一个冲突域下不能有两个同名变量,不同冲突域下可以有同名变量;
我们定义的变量名有可能会和
1.组内成员定义的变量名冲突
2.C++标准库冲突
C++之父提出命名空间这一概念,命名空间起到隔离的作用,主要是隔离全局域的变量/函数/类型名,局部域一般没有必要定义同名变量,也不存在自身和库、自身和他人定义变量名的冲突;
比如下面的代码定义了命名空间prim1,prim2,以及在命名空间prim2嵌套定义了命名空间nest(prim是primary初级这个单词的缩写,写者还是个新手~,nest这个单词有嵌套的意思)命名空间可以将
cpp
namespace prim1 {
int a = 2;
}
//命名空间的嵌套,这个世界是一个无尽的套娃~
namespace prim2 {
int a = 2;
namespace nest {
int a = 2;
}
}
1.2 命名空间的使用
- 展开命名空间(编译时去命名空间搜索),不推荐,因为如果和库里定义的变量名一致且没有引用库的头文件的情况下是不会报错的,但一引头文件就报错了~
- 展开局部,哪个常用展哪个
using 命名空间名::变量名/函数名等 - 访问局部:对于不常使用的命名空间内的变量,在访问的时候按以下格式,
命名空间名::变量名/函数名/类型名等,如果命名空间域是空白,说明是全局域。
cpp
#include <iostream>
namespace prim2 {
int a = 3;
namespace nest {
int a = 4;
}
}
namespace prim1 {
int a = 2;
}
int a = 1;
int main() {
int a = 0;//作用域为函数内部,作用域影响生命周期及访问,出了局部域就销毁了
std::cout << a << std::endl; //局部空间内的操作,优先访问局部域内的变量,打印结果是0;如果局部域没有a的定义,去访问全局域的变量,但不会去搜寻命名空间;因为全局变量的作用域覆盖函数内部,但函数内部的同名变量会覆盖全局变量
std::cout << prim1::a << std::endl;//cout是输出流,endl是endline的缩写,表示换行
std::cout << prim2::a << std::endl;
std::cout << prim2::nest::a << std::endl;//如果是嵌套命名空间内的变量,就嵌套使用"命名空间名prim::变量名/函数名/类型名等"
return 0;
}
输出结果是
c
0
2
3
4
cout, cin是支持自动识别类型(与函数重载有关)的,不像C语言打印时要标明数据类型;但printf, scanf是比cout, cin快的,因为C++是兼容C的,涉及同步问题,打印、读入都是有缓冲区的,C打印、输入可以不管不顾,但是C++要先检查C是否访问当前缓冲区,如果有的话,就使用下一个缓冲区数据单元,因此速度稍慢,如果有大量输入输出,还是printf, scanf比较好~
std是C++标准库(C++ Standard Library),但是注意我们using namespace std;是不够的,因为我们只是把起隔离作用的墙打穿了,但是墙里没有东西也不行,比如我们用cout和endl还要包头文件#include <iostream>,编译的时候会把头文件展开,里面有cout和endl的定义
注意头文件展开和命名空间展开的区别:头文件展开是编译的时候把头文件的内容复制粘贴过来,命名空间展开是将命名空间内定义的变量暴露到全局域。
2.缺省参数
cpp
typedef struct Stack {
int* a;
int top;
int capacity;
}ST;
void InitStack(ST* pst) {
int* a = (int*)malloc(sizeof(int) * 4);
int top = 0;
int capacity = 4;
}
为什么会出现缺省参数这个概念呢?比如上面初始化一个栈的时候,动态数组的情况下,要开辟多少空间成了问题,比如往小了开,刚开始开4个,之后不够了就扩容,但这是事先不知道要用多少空间的情况下;可如果事先知道要用多少空间,比如100个,扩容也是消耗,那么我们使用缺省参数,如果不传,默认开4个,传,就传开的个数,100个,如下所示
cpp
void InitStack(ST* pst,int dafaultcapacity=4) {
int* a = (int*)malloc(sizeof(int) * dafaultcapacity);
int top = 0;
int capacity = dafaultcapacity;
}
int main() {
ST st;
InitStack(&st);
InitStack(&st,100);
return 0;
}
2.1 分类
2.1.1 全缺省
函数的所有参数都可缺省,都有默认值,如下所示;如果传参,从左往右传参,不能指定传参,一个实参默认给第一个,两个实参默认给前两个参数,以此类推
cpp
int add(int a = 0, int b = 0, int c =0) {
return a + b + c;
}
int main() {
cout << add() << endl;
cout << add(1) << endl;
cout << add(1,1) << endl;
cout << add(1,1,1) << endl;
return 0;
}
输出结果
cpp
0
1
2
3
2.1.2 半缺省
部分缺省,部分参数设有默认值,可缺省,因为从左往右传参,所以从右往左缺省;因为从左往右,所以如果传一个实参一定是给第一个参数的,如果出现前面的参数可缺省,后面的参数非缺省,传参要传给后面的参数吗?如果不是,后面的参数没有实参,没有默认值,怎么执行?如果是,破坏了从左向右传参的规则,所以只能从右往左缺省~
举例如下
cpp
void print(int a, int b = 0, int c = 0) {
cout << "a=" << a << " b=" << b << " c=" << c << endl;
cout << "a+b+c=" << a+b+c << endl << endl;
}
int main() {
print(1);
print(1, 1);
print(1, 1, 1);
return 0;
}
输出如下
cpp
a=1 b=0 c=0
a+b+c=1
a=1 b=1 c=0
a+b+c=2
a=1 b=1 c=1
a+b+c=3
2.2 函数声明和定义的半缺省
函数声明时指定缺省参数的默认值,定义时不需要指定
这和编译原理有关,因为编译会把引用的头文件内容拷贝过来,只要有函数的声明,编译即可通过,如果缺省参数不给默认值,而调用的时候没有传实参,编译器就会报错;但是不需要声明和定义都给默认值,防止出现不一致难以处理~
举例说明,在编译过程中,add(10)会自动填充第二个参数为4,即add(10,4)
cpp
int add(int a, int b = 4) {
return a + b;
}
3.重载
函数名相同,参数个数、类型不同,跟形参名无关
- 参数个数不同
cpp
void f() {
cout << "f()" << endl;
}
void f(int a=0) {
cout << "f(int a)" << endl;
}
上述代码编译是没问题的(符合语法),含参调用也是没问题的,但是无参调用会有歧义,如下所示
cpp
int main() {
f();//"f": 对重载函数的调用不明确
return 0;
}
- 参数类型不同
此处说的类型包括类型顺序,也就是从第一个参数开始,相同位置的参数类型要一致
cpp
void print(int a, int b) {
cout << a << " " << b<<endl;
}
void print(double a, double b) {
cout << a << " " << b << endl;
}
int main() {
print(1, 2);
print(1.1, 2.2);
return 0;
}
输出如下
cpp
1 2
1.1 2.2
结合编译原理(号称程序猿的基本素养~ )我们分析一下为什么C语言不支持函数重载,而C++支持函数重载。这是因为函数名修饰规则是不一样的,C语言编译只认函数名,而C++编译Liunx下g++是函数名、参数类型、参数长度都有涉及,所以支持函数重载。

但是返回类型不同,不构成重载,因为函数在调用的时候不一定指明返回类型。
4.引用
入门最重要的内容之一,后续类和对象要反复使用,现阶段最多掌握60~70%,剩下内容要结合类和对象去理解。
4.1 概念
取别名,没有开另外的空间,我个人感觉就是用指针、取地址、解引用本来这个事还需要我们自己去做,现在直接是编译器底层实现好了,一个引用就行了,特点如下:
- 引用在定义时必须
初始化,指针定义时可以不初始化; 不唯一,一个变量可以有多个引用- C++
引用不改变指向,指针可以修改指向;Java中引用可以修改指向,但Java不支持指针。
举例如下,b, c, d都是a的引用,和a等价,位于同一片空间
cpp
int main() {
int a = 0;
int& b = a;
int& c = b;
int& d = c;
cout << "pointer_a: " << &a << endl;
cout << "pointer_b: " << &b << endl;
cout << "pointer_c: " << &c << endl;
cout << "pointer_d: " << &d << endl;
return 0;
}
执行结果如下:
cpp
pointer_a: 00000072A62FFC04
pointer_b: 00000072A62FFC04
pointer_c: 00000072A62FFC04
pointer_d: 00000072A62FFC04
提出引用这个概念是想解决指针的一些复杂问题,但C++指针革命不够彻底;
举例如下,本来写交换两个变量的值这个函数要传指针,现在传引用就可以了
cpp
void Swap1(int* pa, int* pb) {
int temp = *pa;
*pa = *pb;
*pb = temp;
}
void Swap2(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 0, y = 1;
cout << x << " " << y << endl;
Swap1(x, y);
cout << x << " " << y << endl;
x = 0, y = 1;
cout << x << " " << y << endl;
Swap2(x, y);
cout << x << " " << y << endl;
return 0;
}
输出如下
cpp
0 1
1 0
0 1
1 0
4.2 作用
主要是为了减少消耗、提高效率
4.2.1 做参数
大对象/深拷贝类对象
4.2.1.1 大对象传参
当我们传值传参,是将实参拷贝到形参,如果实参过大,导致拷贝造成的代价也会大很多,但引用传参底层是用指针实现的,减少消耗,测试用例如下
cpp
#include <time.h>
struct A {int a[100000]; };
void TestFunc1(A a){}
void TestFunc2(A& a) {}
void TestRefAndvalue() {
A a;//C++中结构体升级为了类,类名可以做类型
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 < 100000; ++i)
TestFunc2(a);
size_t end2 = clock();
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A)-time:" << end2 - begin2 << endl;
}
int main() {
TestRefAndvalue();
return 0;
}
输出结果
cpp
TestFunc1(A)-time:80
TestFunc2(A)-time:1
单位是毫秒,可以看到效率大幅度提升~
4.2.1.2 做输出型参数(形参的改变影响实参)
下面是我们学链表的时候书上很经典的一段代码,LinkList和ListNode*其实是一个类型,这样处理可读性强;而且引用传参,不带头结点尾插、头插、删除第一个节点、清空链表等,就不需要分刚开始空、非空两种情况考虑,使用返回值或指针去处理,代码简单很多,就不容易出错
cpp
typedef struct ListNode {
int data;
struct ListNode* next;
}ListNode, * LinkList;
void InitList(LinkList& L) {
L = NULL;
}
int main() {
LinkList L;
InitList(L);
return 0;
}
4.2.2 做返回值
优点
1.减少拷贝,提高效率(大对象,深拷贝类对象)
2.获取、修改返回值
举例如下,实现对顺序表s第0个位置+5,本来两个函数,现在一个函数一步搞定!
cpp
void SeqModify(SeqList* p, int pos, int x) {
assert(pos >= 0 && pos < 100);
p->a[pos] = x;
}
int& SeqAt(SeqList* p, int pos) {
assert(pos >= 0 && pos < 100);
return p->a[pos];
}
int main() {
SeqList s;
int ret = SeqGet(&s, 0);
SeqModify(&s, 0, ret-1);
SeqAt(&s, 0) -= 1;
return 0;
}
有很多坑,主要是越界问题
先看C语言,下面代码,是通过临时变量实现的,先将函数返回值保存到临时变量,再赋给x,不存在越界问题。
c
int Func() {
int a = 0;
return a;
}
int main() {
int x = Func();
return 0;
}
如果返回值是函数局部变量的引用,函数调用结束,栈帧销毁,局部变量生命周期结束,我们再去访问这片空间就是越界,得到的值是正确值或随机值,拿到正确的值不代表这个行为是正确的,虽然我们访问到的值是正确的,是因为函数栈帧刚销毁,还没有被其它函数栈帧覆盖,而编译器也没较真,我们侥幸越界访问到了正确的值,如果编译器和我们较真,不允许越界访问,那程序可能会报错;举例如下
cpp
int& Func() {
int a = 0;
return a;
}
int main() {
int x = Func();
return 0;
}
但是如果返回值作为全局变量、静态变量、malloc(动态分配一般在堆上,但谨慎使用)、常量等的引用,这些出了函数不会被销毁,因此访问不是越界;举例如下
cpp
int& Func() {
static int a = 0;
return a;
}
int main() {
int x = Func();
return 0;
}
注意上面两种情况都是讲函数返回值赋给变量x,是一次性行为,接下来我们接着看
举例
cpp
int& Func() {
int a = 0;
return a;
}
int main() {
int& x = Func();
cout << x << endl;
return 0;
}
输出结果
cpp
0
变例
cpp
int& Func() {
int a = 0;
return a;
}
int main() {
int& x = Func();
cout << "abcdefg" << endl;
cout << "abcdefg" << endl;
cout << "abcdefg" << endl;
cout << "abcdefg" << endl;
cout << "abcdefg" << endl;
cout << "abcdefg" << endl;
cout << x << endl;
return 0;
}
输出结果
cpp
abcdefg
abcdefg
abcdefg
abcdefg
abcdefg
abcdefg
32763
变例
cpp
int& Func() {
int a = 0;
return a;
}
int main() {
int& x = Func();
printf("12344567899685746352\n");
printf("12344567899685746352\n");
printf("12344567899685746352\n");
cout << x << endl;
return 0;
}
输出结果
cpp
12344567899685746352
12344567899685746352
12344567899685746352
21
我们可以看到三种情况下访问到的x不同,因为我们拿到的是a的引用,这片空间已经还给操作系统了,操作系统可以将这片空间分配给其它进程使用,当建立新的函数栈帧将原来的函数栈帧覆盖,不同的函数栈帧,越界访问到的值就会有所不同,就好像你的手机号不用了还会重新投入市场使用,你再去给这个号码打电话,也不知道接的人是谁。
4.3 引用的权限问题
引用,权限不能放大,可以缩小或平移
下面是平移
cpp
int main() {
int a = 0;
int& b = a;
return 0;
}
下面是缩小,a改变,b也会变;但是不能通过修改b来改变a
cpp
int main() {
int a = 0;
const int& b = a;
return 0;
}
这是赋值
cpp
int main() {
int a = 0;
const int b = a;
int c = b;
return 0;
}
下面是放大权限,不能放大权限,本来a是不可以修改的,现在b是a的引用,b可以修改,权限放大了,VS2022编不过去

下面还是权限放大问题,d是double类型,c是int型,要先强制转换数据类型(不同类型转换会借助临时变量,同一类型不需要),c才能成为d的引用,但是强转是通过临时变量(具有常性)也就是常值来辅助的,所以说右边相当于是常量,不可修改,权限放大

上面的问题改成const常量即可,权限平移,如下所示
cpp
int main() {
int a = 1;
double d = 1.1;
const int& c = d;
return 0;
}
看一个综合起来的例子~
cpp
int Func1() {
static int a = 0;
return a;
}
int& Func2() {
static int b = 0;
return b;
}
int main() {
int x1 = Func1();//赋值
int& y1 = Func1();//权限放大
const int& z1 = Func1();//平移
int x2 = Func2();//赋值
int& y2 = Func2();//平移
const int& z3 = Func2();//缩小
return 0;
}
4.4 与指针区别
面试常考点

如下图所示,引用和指针在底层实现基本一致

牛刀小试~
(阿里巴巴2015笔试题) 关于引用以下说法错误的是( )
A.引用必须初始化,指针不必
B.引用初始化以后不能被改变,指针可以改变所指的对象
C.不存在指向空值的引用,但是存在指向空值的指针
D.一个引用可以看作是某个变量的一个"别名"
E.引用传值,指针传地址
F.函数参数可以声明为引用或指针类型
E
5.内联函数
5.1 宏定义的缺陷、内联函数的优点
我们知道,调用函数是要开辟空间建立函数栈帧、保持寄存器、传参、传返回值等,如果频繁调用某一个函数,建立、销毁栈帧是有消耗的,C语言下使用宏定义来解决这个问题,宏定义可维护性强,可以复用修改,一个地方改,其它地方都改。举例如下
c
int Add(int a, int b) {
return a + b;
}
先看C语言的宏定义,宏定义在预处理阶段直接展开,不需要建立函数栈帧,但是不能太复杂,否则代码可读性变差
我们把上面的函数改成宏定义如下
c
#define Add1(x,y) ((x)+(y))
x, y外面以及整体的括号都必不可少,如果没有最外层的括号,如下所示,会出问题
c
int a = 10 * Add1(3, 4);//等价于10*3+4,因为宏是直接替换
如果没有x, y外面的括号,举例如下
c
int e = a | Add1(b, c) & d;//等价于int e=a|b+c&d, 但要注意+的优先级比a, d高,所以先算加,再|、&,这样和我们本意不符
宏定义也没有类型安全的检查,所以我们可以看到宏定义是有很多坑可以踩的,是复杂的,C++提出内联函数,如下所示,在函数定义前加inline关键字即可,和宏是等价的,预处理时展开。
内联函数优点,首先宏定义是解决建立函数栈帧,那么inline是对宏的优化,也不需要建立栈帧;接着,宏比较复杂、在定义稍微复杂的运算时可读性下降,就引入简单、可读性强的内联函数,简单也就不那么容易出错;再者,宏不能调试,直接替换,怎么调嘞,所以内联还能调试。
内联函数的优点
1.不需要建立函数栈帧
2.不复杂,不容易出错
3.可读性强
4.能调试
5.2 内联函数的适用场景
c
inline int Add(int a, int b) {
return a + b;
}
但是内联函数、宏定义这些都适合短小、频繁使用的函数,如果是大型函数使用宏定义,可能出现代码膨胀的问题,举例分析如下:
c
int Add(int a, int b) {
return a + b;
}
int main() {
int a=Add(1, 2);
return 0;
}
我们来看一下上面这段代码的反汇编,如下所示,函数调用是跳转指令,调用200次就跳转200次,函数主体部分的代码不会增加,假设我们调用200次函数Add,Add实现指令是60条(因为我们分析大型函数问题,此处假设不要太当真~);假设每次调用是4条汇编指令,一共是4* 200+50条汇编指令;我们接着来看宏定义/内联函数,我们知道宏定义、内联函数是展开,调用200次,展开200次,那就是200*50次,所以会出现代码膨胀问题 ~会导致链接生成的可执行程序很大,拿软件开发来说,我们最后会生成一个安装包用于更新等,但是内存总是有限的,我们不希望生成的安装包很大,影响市场。举个例子,之前王者荣耀在ios上700-800MB,但在安卓上是2-3G,安卓底层是Linux,Linux本身很优秀,但是安卓就有点跟不上,可能跟Java底层的虚拟机实现有些关系;市值最高苹果的硬件、swift实现的ios软件做的都很好。

所以编译器会卡内联的使用,确保最终生成的可执行程序不至于过大,我们来看内联函数Add的展开如下
(默认debug模式下,inline不起作用,否则不方便调试,查询下面的汇编代码过程如下
)

当我们把内联函数改成下面比较长的函数,
cpp
inline int Add(int a, int b) {
cout << "11111111111111" << endl;
cout << "11111111111111" << endl;
cout << "11111111111111" << endl;
cout << "11111111111111" << endl;
cout << "11111111111111" << endl;
cout << "11111111111111" << endl;
cout << "11111111111111" << endl;
return a + b;
}
汇编代码如下,我们看到是call调用函数,也就是说,在编译器看来,我们写的inline内联只是一个建议,如果这个函数很长或者递归,那就一票否决~

5.3 inline声明和定义
注意inline的声明和定义不能分开在头文件、.cpp文件分别进行,我们知道预处理会展开inline,但是test.cpp中展开Func.h只有Add的声明,没有定义,没办法展开inline,所以编译不了;只能在头文件中同时声明和定义

6.语法糖
6.1 auto自动匹配类型
C++里面加上命名空间,很多类型就会特别长,这时候auto就出现了,auto可以根据右边表达式的值自动匹配所修饰变量的类型,举例如下
cpp
int main() {
auto x = 1 + 1.1;//有隐式类型转换,长度不同,精读低向精读高看齐,int向double看齐,1转换为double型和1.1进行加法运算,得到的结果是double型
cout << typeid(x).name() << endl;//用于获取变量类型名的函数
return 0;
}
输出结果如下
cpp
int
变量类型较长举例如下
cpp
#include <iostream>
#include <map>
#include <string>
#include <vector>
using namespace std;
int main() {
vector<int> v;//vector是顺序表
//vector<int>::iterator it = v.begin();
// 等价于
auto it = v.begin();
std::map<std::string, std::string> dict;
//std::map<std::string, std::string>::iterator dit = dict.begin();
//等价于
auto dit = dict.begin();
cout << typeid(dit).name() << endl;
return 0;
}
但是也存在类型一致的问题,比如auto* 右边必须是指针,比如下面1是常量const int型,左边是指针,类型不一致,不可以,举例如下

6.2 for
之前如果我们要把一个数组元素都扩大为原来的2倍是这样写的
cpp
int main()
{
/*auto x = 1 + 1.1;
cout << typeid(x).name() << endl;*/
int arr[] = { 1,2,3,4,5 };
for (int i = 0; i < sizeof(arr) / sizeof(int); i++) {
arr[i] *= 2;
cout << arr[i] << " ";
}
return 0;
}
C++可以这样写
cpp
int main() {
int arr[] = { 1,2,3,4,5 };
for (auto& e : arr) {//自动迭代,自动判断结束
e*=2;
cout<<e<<" ";
}
return 0;
}
输出结果如下
cpp
2 4 6 8 10
这个地方传参不能传数组,会退化为指针,要传数组名,也就是数组首元素地址

6.3 nullptr
接下来我们来看一下C里的NULL,举例如下
cpp
void Func1(int) {//注意这个地方没有给接收实参的形参,因为这两个函数都用不到实参,只是做类型匹配,一般用不到实参,只用到类型,可以不设置形参
cout << "int" << endl;
}
void Func1(int*) {
cout << "int*" << endl;
}
int main()
{
Func1(0);
Func1(NULL);
return 0;
}
输出结果如下
cpp
int
int
我们知道NULL是空指针,按理来说应该输出int*, 但结果是int,这是因为把NULL当常量整型处理了,因为NULL表示地址空间中第0个单元的地址,NULL实际是一个宏, 在传统的C头文件(stddef.h)中, 可以看到如下代码:
c
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++引入nullptr,看作(void*)0,Func1(nullptr);的输出结果是"int*".
但是C++还是保留了NULL原有的用法,因为应用很广泛,一旦推翻某个规则就可能导致之前开发的软件等出问题,一般都是向前兼容,小问题做修补,除非是致命的大问题,不然一般都会保留;即使是错了也不能删,不能改,包括Java过去有些语法不合理,现在依然不合理,这么多年只有python3是不兼容python2的,被骂的不轻~

完结撒花~✿✿ヽ(°▽°)ノ✿
