C++历史往事
C语言之父是本贾尼
C++是在C语言的基础上生长出来的,因此C++是兼容C语言的,可以在C++里用C语言的代码

创建C++文件
在VS上创建C++文件
先去创建项目,再去创建文件:与之前的创建C语言文件一样,将文件后缀改为.cpp即可


C++发展过程
在1989年该阶段,C语言和C++发展已经不是本贾尼博士一个人了,而是有ANSI和ISO国际标准化组织一起完成的,在1994年提出第一个标准草案,该草案保留了本贾尼博士的最初定义的所有特征外,还增加了一些新的特征
STL很重要,我们前面做的用两个队列实现栈和两个栈实现队列时还需要先去实现轮子才能造车,在STL上可以直接造车,不需要造轮子
解释造轮子和造车:
用两个队列实现栈举例说明
造轮子是指的是先要去造两个队列,造车:才能去造栈也就是造车;反之两个栈实现队列也是一样
C++现有的三个大的版本:C98、C11、C20版本,C23本来也是大版本,由于没有网络库,因此不算在大版本里
C++参考三个文档:
https://legacy.cplusplus.com/reference/
这个更实用一些,建议用这个
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/
看文档去看英文版,不会的可以去查翻译
C++找工作错位竞争:大厂找测开,小厂找开发
C++学习建议:1.听课2.作业3.练习案例+博客笔记
C++书籍:都是中后期的书籍
C++primer :中后期可以将它作为语法字典用
STL源码剖析、Effctive C++
C++兼容C语言
C++兼容C语⾔绝⼤多数的语法,所以C语⾔实现的hello world依旧可以运⾏,C++中需要把定义⽂件代码后缀改为.cpp。
vs编译器看到是.cpp就会调⽤C++编译器编译,linux下要⽤g++编译,不再是gcc
命名空间
先看个例子:
cpp
#include<stdio.h>
#include<stdlib.h>
//不加头文件stdlib.h,rand正常打印,加了报错:"rand": 重定义;以前的定义是"函数"
//这是因为该头文件在预处理阶段会将该头文件里的内容拷贝一份到这里来,而拷贝的内容里有个叫rand的函数
//它会与下面这个全局变量rand造成"命名冲突"这时就需要命名空间来解决问题
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
在以后写大项目时可能是几个人写,最后组合到一起,可能你写的代码和我写的代码会有上述情况,使用命名空间就可以解决命名冲突的问题,用到的关键字namespace。
namespace的定义
与定义结构体差不多,但在最后的{}后不加' ; ' 号,具体格式如下:
cpp
namespace 命名空间的名字 //改名字可以任意取,一般是由自己名字的缩写构成
{
}
//{}里面就是命名空间的成员。命名空间中可以定义变量/函数/类型等。
将上面那个报错的代码改正如下:
cpp
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
namespace mf
{
int rand = 10;
};
}
int main()
{
// 这⾥默认是访问的是在头文件stdlib.h中全局的rand函数指针,所以用%p来打印
printf("%p\n", rand);
//:: 是域作用限定符 --- 调用命名空间里的成员要用到的
// 这⾥指定mf命名空间中的rand
printf("%d\n", mf::rand);
}
除了 在命名空间中可以定义变量外,还可以定义类型、函数等,下面是调用的代码:
cpp
#include<stdio.h>
#include<stdlib.h>
namespace mf
{
//变量
int rand = 10;
//函数
int Add(int left, int right)
{
return left + right;
}
//类型
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
// 变量
printf("%d\n", mf::rand);
//函数
//Add(1, 2); // --- err
mf::Add(1, 2);
//类型(结构体属于自定义类型)
struct mf :: Node node;//注意类型的调用格式
}
命名空间的本质是定义了一个域,该域与全局域是相互独立的;不同的域可以定义同名变量,同一个域不能定义同名变量,因此前面的rand的命名冲突就解决了
域的作用
C++域中有局部域、全局域、类域、命名空间域;域的作用改变编译时语法查找⼀个变量/函数/
类型出处(声明或定义),做到名字隔离(也叫域隔离),解决命名冲突的问题。
局部域和全局域不仅有域隔离作用,还会影响生命周期;类域与命名空间域不会影响生命周期
其中类域与命名空间域里的成员是全局的,只是被域给隔离了。
验证命名空间域的成员时全局的
cpp
#include<stdio.h>
#include<stdlib.h>
namespace mf
{
//变量
int rand = 10;
//函数
int Add(int left, int right)
{
return left + right;
}
//类型
struct Node
{
struct Node* next;
int val;
};
}
void func()
{
printf("%d\n", mf::rand++);
}
int main()
{
func();
printf("%d\n", mf::rand);
}
在func函数里还可以调用命名空间域里的变量rand,说明rand时全局变量,只不过被命名空间域给隔离了
关于编译时的查找解释
在查找出处时先从局部去找,找不到再去全局找,也就是从该行代码往上找,直到找到为止
举个例子:
cpp
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
const char* str = "hello world";
//编译时的查找
strlen(str);//代码在编译时需要找到它的出处,若不加头文件会找不到出处,会报错
}
总结:
cpp
#include<stdio.h>
int x = 666;
namespace mf
{
int x = 777;
}
void func()
{
int x = 2;
}
int main()
{
int x = 3;
printf("%d\n", x);//访问局部的
printf("%d\n", mf::x);//访问命名空间的
printf("%d\n", ::x);//想要访问全局的让它的左边为空
//不能访问func函数里的,因为它是局部的,出了就被销毁了
return 0;
}
命名空间只能定义在全局,并且命名空间可以嵌套使用
cpp
#include<stdio.h>
//可以命名空间里嵌套命名空间
namespace mf
{
//mf1
namespace mf1
{
int rand = 1;
int Add(int left, int right)
{
return left + right;
}
}
// mf2
namespace mf2
{
int rand = 2;
int Add(int left, int right)
{
return (left + right) * 10;
}
}
}
int main()
{
//////////////////
//namespace mf1//命名空间只能定义在全局里 --- err
//{
// int rand = 1;
// int Add(int left, int right)
// {
// return left + right;
// }
//}
////////////////
printf("%d\n", mf::mf1::rand);//调用嵌套时要加上多个::命名空间限定符
printf("%d\n", mf::mf2::rand);
printf("%d\n", mf::mf1::Add(2, 3));
printf("%d\n", mf::mf2::Add(2, 3));
return 0;
}
在以后写大项目时,几个人写的和到一个命名空间里,为了防止该空间里还有命名冲突的问题会使用到嵌套,做到隔离。
项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。
举个例子:
cpp
#include<stdio.h>
#include"Queue.h"
#include"Stack.h"
// 全局定义了⼀份单独的Stack
typedef struct Stack
{
int a[10];
int top;
}ST;
void STInit(ST* ps) {}
void STPush(ST* ps, int x) {}
int main()
{
// 调⽤全局的
ST st1;
STInit(&st1);
STPush(&st1, 1);
STPush(&st1, 2);
printf("%zd\n", sizeof(st1));
// 调⽤mf namespace的
mf::ST st2;
printf("%zd\n", sizeof(st2));
mf::STInit(&st2, 4);
mf::STPush(&st2, 1);
mf::STPush(&st2, 2);
return 0;
}
这里是部分代码,剩下的代码都在VS里,打印的结果如下:

这样就解决了在多个文件中的命名冲突问题
C++标准库都放在⼀个叫std(standard)的命名空间中,为了防止在程序中和标准库中有命名冲突的问题
命名空间使⽤
编译查找⼀个变量的声明/定义时,一般先在当前局部找,然后再去全局查找,不会到命名空间⾥⾯去查找。所以下⾯程序会编译报错。所以我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
• 指定命名空间访问,项⽬中推荐这种⽅式。
cpp
int main()
{
//// 编译报错:error C2065: "a": 未声明的标识符
//printf("%d\n", a);
//方法1:指定命名空间访问
printf("%d\n", mf::a);
return 0;
}
• using将命名空间中某个成员展开,项⽬中经常访问的但不存在冲突的成员推荐这种⽅式。
cpp
//方法2使用using将命名空间中某个成员展开
using mf::a;//也就是将它展开让它成为全局变量
int b = 10;
int main()
{
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", a);
printf("%d\n", a);
//b++;//报错要加mf::
mf::b++;//报错要加mf::
return 0;
}
• 展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
cpp
//方法3:展开命名空间中的所有成员
using namespace mf;
//int a = 10;//若是出现了命名冲突,他会报错,在大项目中不太适用
int main()
{
printf("%d\n", a);
printf("%d\n", b);
return;
}
C++输⼊&输出
• 是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输
出对象。在C++中要用到该头文件
数据只有在内存中才会由整型、浮点型等其他类型进行运算,在终端/控制台上只有字符
std::cin 是 istream 类的对象,它主要⾯向窄字符(narrow characters (of type char))的标准输⼊流。
• std::cout 是 ostream 类的对象,它主要⾯向窄字符的标准输出流。
std:使用C++时所用到的标准库
其中cin和cout解释:
cin是指先将你输入的数据解析成一串字符串再转换成整型、浮点型等类型再去输入
cout是指先将内存中的数据转换成一串字符串在输出到终端/控制台上
<<是流插⼊运算符 --- 将数据输出到终端上,也就是打印的功能
>>是流提取运算符 --- 从键盘上输入数据到程序中,也就是输入的功能
特点:这两个运算符支持连续流插入和连续的流提取;并且该运算符是自动识别类型的,不需要输入占位符
std::endl : 等价于C语言中的换行符 '\n',是end line的缩写
举例:
cpp
#include <iostream>
int main()
{
int a = 12;
double b = 3.14;
char c = 'X';
//流插入运算符
std::cout << "hello world\n";
std::cout << a << " " << b << " " << c << "\n";
std::cout << a << " " << b << " " << c << std::endl;
//流提取运算符
std::cin >> a >> b >> c;
std::cout << a << " " << b << " " << c << std::endl;
return 0;
}
运行结果:

在C++里也可以用scanf 和printf,
• cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要通过命名空间的使⽤⽅式去⽤他们。
• ⼀般⽇常练习中我们可以using namespace std,实际项⽬开发中不建议using namespace std。
• 这⾥我们没有包含<stdio.h>,也可以使⽤printf和scanf,在包含<iostream>间接包含了。
vs系列编译器是这样的,其他编译器可能会报错(比如在使用Linux时不加对应的头文件会报错,解决办法:缺哪个头文件加上即可), 这两个在使用时需要将对应的类型占位符加上。
• 若是要对输入的浮点型进行精度控制建议用scanf和printf,简单
代码如下:
cpp
#include <iostream>
//若是嫌弃每次输入cout 和cin 可以将它们的命名空间展开,对代码进行优化
using std::cout;
using std::cin;
using std::endl;
int main()
{
int a = 12;
double b = 3.14;
char c = 'X';
//流插入运算符
cout << "hello world\n";
cout << a << " " << b << " " << c << "\n";
cout << a << " " << b << " " << c << endl;
//流提取运算符
cin >> a >> b >> c;
cout << a << " " << b << " " << c << endl;
scanf("%d %lf %c", &a, &b, &c);
printf("%d %.2lf %c\n", a, b, c);
return 0;
}
C++的cout等的运行效率会低一些,原因:将数据插入后,数据会先到数据的缓冲区里,遇到刷新标志后才会将插入的数据刷出去到终端/控制台上,若是这一个缓冲区没有遇到刷新标志,在执行下一句操作时它会主动将该缓冲区的数据刷新出去,再让下一行代码进入到自己的缓冲区进行插入数据,这个过程需要 一定的成本,所以C++的std::cout 和 std::cin的运行效率低。
若想要提高效率可以每次在main函数的开始输入下面三行代码,它让C++变得不包含C了,从而提高效率了
代码如下:
cpp
#include<iostream>
using namespace std;
int main()
{
// 在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下3⾏代码
// 可以提⾼C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
缺省参数
在函数声明或定义时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参
则采⽤该形参的缺省值,否则使⽤指定的实参 ,缺省参数分为全缺省和半缺省参数。(有些地⽅把缺省参数也叫默认参数)
例如:
cpp
#include <iostream>
#include <assert.h>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func();
// 没有传参时,使⽤参数的默认值
Func(10);
// 传参时,使⽤指定的实参
return 0;
}
全缺省和半缺省
全缺省就是全部形参给缺省值; 半缺省就是部分形参给缺省值。
C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
例如:
带缺省参数的函数调⽤,C++规定必须从左到右依次给实参,不能跳跃给实参。
cpp
#include<iostream>
using std::cout;
//全缺省
void Func1(int a = 10, int b = 2,int c = 40)
{
cout << a << ' ' << b << ' ' << c << std::endl;
}
//半缺省 --- 从右往左至少有一个缺少参数,并且是连续的
void Func2(int a, int b = 20, int c = 20)
{
cout << a << ' ' << b << ' ' << c << std::endl;
}
////错误半缺省 --- 必须是从右往左缺省,不能只缺省中间的
//void Func2(int a = 10, int b, int c = 20)
//{
// cout << a << ' ' << b << ' ' << c << std::endl;
//}
int main()
{
//全缺省的调用:
Func1();
Func1(1);
Func1(1, 2);
Func1(1, 2, 3);
//Func1(, 200, );//err 不支持跳跃调用
//半缺省的调用
//Func2();//err --- 半缺省在调用时没有的缺省的必须要有实参,并且不能跳跃调用
Func2(100, 200);
Func2(100, 200, 300);
return 0;
}
缺省参数的实践应用:
用顺序表创建的栈中当我不知道要创建多大的空间时我们可以使用缺省参数实现,它的缺点是要不断的扩容;若已经知道要空间大小,可以直接调用栈初始化显示实现
代码如下:
cpp
//Test.cpp
#include"Stack.h"
int main()
{
//一般情况下是不用去管这里初始化时需要多大的空间,因为在初始化时已经设置好了缺省参数。
mf::ST st;
mf::STInit(&st);
// 确定知道要插⼊100个数据,就不用缺省值了,直接用这里的显示表示即可
//mf::ST st;
//mf::STInit(&st);
//for (size_t i = 0; i < 100; i++)
//{
// mf::STPush(&st, i);
//}
return 0;
}
//Stack.cpp
#include"Stack.h"
namespace mf
{
void STInit(ST* ps, int n)//前面声明给了缺省参数,这里就不再给了
{
assert(ps);
ps->a = (STDataType*)malloc(n * sizeof(STDataType));
ps->top = 0;
ps->capacity = n;
}
}
//Stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
namespace mf
{
typedef int STDataType;
typedef struct Stack
{
STDataType * a;
int top;
int capacity;
}ST;
void STInit(ST* ps, int n = 4);//缺省参数4,这里必须给缺省参数
void STPush(ST* ps, STDataType x);
}
函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现 ,规定必须函数声明给缺省值。(案例解释在上面代码里)
做人不能做缺省参数
函数重载
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的 形参 不同 ,可以是参数个数不同或者类型不同 。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的。
cpp
#include<iostream>
using namespace std;
// 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;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同 --- 本质还是参数的类型不同
void f(int a, char b)
{
cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
int main()
{
Add(10, 20);
Add(10.1, 20.2);
f();
f(10);
f(10, 'a');
f('a', 10);
return 0;
}
运行结果:

函数的返回值不构成函数重载
cpp
////返回值不同不能作为重载条件,因为调⽤时也⽆法区分
void fxx()
{
}
int fxx()//err -- 报错
{
}
int main()
{
fxx();
return 0;
}
再看看下面两个函数:
cpp
// 下⾯两个函数构成重载 --- 加了缺省参数的导致形参个数不同
void f1()
{
cout << "f()" << endl;
}
void f1(int a = 10)
{
cout << "f(int a)" << endl;
}
int main()
{
f1();//err // f()但是调⽤时,会报错,存在歧义,编译器不知道调⽤谁
return 0;
}
函数重载体现在Swap函数
函数重载体现在Swap函数,在C语言中无法使用两个函数名相同但类型不同的交换函数,但在C++里就可以实现
例如:
cpp
#include<iostream>
using namespace std;
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void Swap(double* c, double* d)
{
double tmp = *c;
*c = *d;
*d = tmp;
}
int main()
{
int a = 10, b = 20;
double c = 2.2, d = 1.1;
cout << "交换前:" << endl;
cout << a << " " << b << endl << c << " " << d << endl;
Swap(&a, &b);
Swap(&c, &d);
cout << "交换后:" << endl;
cout << a << " " << b << endl << c << " " << d << endl;
return 0;
}
运行结果:

引⽤的概念和定义
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名 ,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。⽐如:水浒传中李逵,江湖上⼈称"⿊旋⻛";
使用方式:
类型& 引⽤别名 = 引⽤对象;
举例:
cpp
#include<iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;//给变量取别名
//给别名取别名
int& c = b;
int& d = c;
d++;
cout << "a = " << a << endl;//11
cout << "b = " << b << endl;//11
cout << "c = " << c << endl;//11
cout << "d = " << d << endl;//11
return 0;
}
结果如下:

通过结果观察,验证了它引⽤的变量是共⽤同⼀块内存空间
区分#define、typedef 和 引用
cpp
#define N 10;//是宏定义,在预处理阶段将参数N替换成常数10
typedef unsigned int uint;//给类型取别名
//引用是给变量取别名
引用还可以给地址取别名
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int*p = &a;
int*& p1 = p;
int*& p2 = p1;
int e = 111;
p2 = &e;
cout<< *p2 << endl;
return 0;
}
结果:

对地址取别名,改变别名地址中的值就会改变原地址中的值
引用适用于Swap函数
例如:
cpp
#include<iostream>
using namespace std;
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10, b = 20;
cout << "交换前:" << endl;
cout << a << " " << b << endl;
Swap(a, b);
cout << "交换后:" << endl;
cout << a << " " << b << endl;
return 0;
}
形参是实参的别名,改变了形参就改变了实参
我们前面学的链表功能实现用的二级指针可以用引用来替代,代码如下:
cpp
#include<iostream>
using namespace std;
typedef struct SeqList
{
int a[10];
int size;
}SLT;
//使⽤C++引⽤替代指针传参,⽬的是简化程序
void SeqPushBack(SLT& sl, int x)
{
}
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode, * PNode;
// 指针变量也可以取别名,这⾥LTNode*& phead就是给指针变量取别名
// 这样就不需要⽤⼆级指针了,简化了程序
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)
{
PNode newnode = (PNode)malloc(sizeof(LTNode));
newnode->val = x;
newnode->next = NULL;
if (phead == NULL)
{
phead = newnode;
}
else
{
//...
}
}
int main()
{
PNode plist = NULL;
ListPushBack(plist, 1);
return 0;
}
引用的三个特性:
• 引⽤在定义时必须初始化
• ⼀个变量可以有多个引⽤,也就是可以有多个别名
• 引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体,也就是一旦对一个变量取了别名,该别名就不能在被其他的变量所引用,也可以理解为在C++里引用不能改变指向 ;这说明在C++中,引用不能完全代替指针。在Java中引用可以改变指针的指向
代码如下:
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 10;
////int& b;//引用b没有初始化报错
//可以有多个别名
int& c = a;
int& d = a;
int& e = a;
int& f = a;
//现在想要将a的别名f改成b的别名,不能改,在运行时会报错
int b = 20;
//int& f = b;//err
int g = b;//这个是将b的值赋给g,而不是引用,打印其地址时只会打印它自己的地址
cout << &a << endl;
cout << &c << endl;
cout << &d << endl;
cout << &e << endl;
cout << &b << endl;
cout << &f << endl;
cout << &g << endl;
return 0;
}
打印结果如下:

引⽤在实践中主要作用是:
在引⽤传参和引⽤做返回值中减少拷⻉提⾼效率
我们前面学的将实参传给形参要是传值的话,这里需要将实参的值一个一个拷贝给形参,一个整型为4个字节,一共1000个数据要传4000个字节的大小,这使得效率降低;
改为传指针,传指针不管传多少,它的大小都为4/8个字节,但这样的效率还是很低。用引用不会去传而是直接调用原位置中的数据。因此使用引用减少拷贝提高效率。
改变引⽤对象时同时改变被引⽤对象。
比如这里的Swap函数定义的形参作为引用对象时,实现交换的功能的同时改变了引用对象;在调用swap函数时给的实参经过函数定义的交换后它俩的值也会被改变,所以实参就是被引用对象。
这两个作用的实例如下:
cpp
#include<iostream>
using namespace std;
////传值
//void Swap(int rx, int ry)
//{
// int tmp = rx;
// rx = ry;
// ry = tmp;
//}
////传地址
//void Swap(int* rx, int* ry)
//{
// int tmp = *rx;
// *rx = *ry;
// *ry = tmp;
//}
//引用
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
int main()
{
int x = 0, y = 1;
cout << x << " " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
下面是引用传返回值的提高效率的操作代码:
cpp
#include<iostream>
#include<assert.h>
using namespace std;
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST& rs, int n = 4)
{
rs.a = (STDataType*)malloc(n * sizeof(STDataType));
rs.top = 0;
rs.capacity = n;
}
// 栈顶
void STPush(ST& rs, STDataType x)
{
// 满了, 扩容
if (rs.top == rs.capacity)
{
printf("扩容\n");
int newcapacity = rs.capacity == 0 ? 4 : rs.capacity * 2;
STDataType* tmp = (STDataType*)realloc(rs.a, newcapacity *
sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
rs.a = tmp;
rs.capacity = newcapacity;
}
rs.a[rs.top] = x;
rs.top++;
}
// int STTop(ST& rs) 若是这里返回的是值时,STTop会先将返回给临时变量,其次才会给返回值,并且若想要对得到的栈顶元素进行修改,在返回的值里是不可能实现的,因为它修改的是临时变量,但若是返回的是别名的话可以修改栈顶数据,因为它是对数组中的值取得别名并且没有中间的临时变量
int& STTop(ST& rs)
{
assert(rs.top > 0);
return rs.a[rs.top - 1];//top为数据的有效个数,记得-1
}
////用传指针实现 --- 传指针实现写代码很麻烦,所以用引用方便一些
//int* STTop(ST& rs)
//{
// assert(rs.top > 0);
// return &(rs.a[rs.top - 1]);
// ////等价于return rs.a + rs.top - 1;
//}
int main()
{
ST st1;
STInit(st1);
STPush(st1, 1);
STPush(st1, 2);
cout << STTop(st1) << endl;
STTop(st1) += 10;
cout << STTop(st1) << endl;
//cout <<*(STTop(st1)) << endl;
//*(STTop(st1)) += 10;
//cout << *(STTop(st1)) << endl;
return 0;
}
引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
返回值一定可以用引用和地址来返回吗?不,看下面这个案例:
cpp
#include<iostream>
using namespace std;
//int* func()
//{
// int ret = 10;
// ret += 20;
// return &ret;
//}
int& func()
{
int ret = 10;
ret += 20;
return ret;
}
int main()
{
cout << func() << endl;
return 0;
}
该函数不能使用返回值,因为变量ret是局部变量,出了函数就销毁了,但程序的运行结果没有报错,即使是用引用进行返回也不行。
越界是不一定报错的
越界读不报错
越界写不一定报错,他一般是通过抽查进行报错的

在VS上,他会在数组的后面设置两个抽查位,若这两位被修改了,会越界报错;若没被修改,即使越界了他也不会报错
这个也可以解释前面为什么ret是在func函数中的局部变量,但是返回它却没有报错,说明它没有改变抽查位,所以即使越界了也不会报错。
引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代(比如二叉树的链式结构的实现中左右孩子节点的指向会改变,不能用引用,要用指针)。
C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点:
C++引⽤定义后不能改变指向, Java的引⽤可以改变指向。
在一些书上,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针
const引⽤
const引⽤的特点:
- 可以引⽤⼀个const对象,但是必须⽤const引⽤。
- const引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩和平移,但是不能放⼤。
- 使用普通对象用const引用取别名后,原来普通对象可以不用const引用。
举个例子来验证这三点:
cpp
#include<iostream>
using namespace std;
int main()
{
//1.权限不能放大
const int& a = 10;
////a++;//---err
//2. 权限可以缩小
int b = 20;
const int& rb = b;
////rb++;//--- err
//3.
b++;
//自测:下面是否是权限放大?
const int& a = 10;
int b = a;
//不是,是将a赋值拷贝给b操作
return 0;
}
权限的放大和缩小用在两个方面:一个是引用,一个是地址
const在之前修饰的是指向的内容;在 之后修饰的类型变量本身;
下面是关于地址权限的放大缩小代码:
cpp
#include<iostream>
using namespace std;
int main()
{
//权限不能放大
int a = 10;
const int* pa = &a;
//int* pa1 = pa; //err
//权限可以缩小
int b = 10;
int* p3 = &b;
const int* p4 = p3;
//自测:下面是否是权限的放大?
int* const p5 = &b;
int* p6 = p5;
//不是,因为const在*之后,修饰的变量本身p5,不是指向的内容
return 0;
}
通过引用和指针的权限问题我们可知:
引用和指针限定的是指向的内容,它们只有在调用同一个对象时才会有权限的放大和缩小
判断权限的放大/缩小方法 : 先得调用的是同一个对象,其次先看它在指针/引用时是否是可读 / 可写/可读可写,再去看修改后的是否是可读 / 可写/可读可写,若前面的 是只 读的也就是const在*之前,后面的也要变为可读的才能使得权限不被放大。若是可读可写的const在 * 之后,后面的代码既可以是可读可写的这是权限的平移,也可以是可读的/可写的,这是权限的缩小。但若是前面只读/只写的,后面却是可读可写的,这是权限的放大,是错误的
临时对象造成的权限放大问题:
编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,C++中把这个未命名对象叫做临时对象 。在C++里所谓的对象就是变量,只是改了个叫法而已。
先看代码:
cpp
#include<iostream>
using namespace std;
int main()
{
int a = 30;
//int& b = a * 10; //err 原因:表达式a*10求结果时会产⽣临时对
//象存储结果,也就是此时表达式a*10已经存储到了临时对象里,⽽C++规定临时对象具有常性(被const修饰,在类型前面带一个const)而去引用时却用的时不带const修饰的,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
const int& b = a * 10;//加上const
return 0;
}
const可以引用const对象、普通对象和临时对象,分别对应三个不同的内容
const在以后学习的应用:
若是创建的函数中形参不像Swap函数那样发生改变,建议使用const进行修饰
举个实例:
cpp
#include<iostream>
using namespace std;
void Func(int num)
{
//.....
}
int main()
{
int a = 30;
const int& b = a * 10;
double d = 12.34;
const int& rd = d;
Func(b);
Func(rd);
return 0;
}
这样做的好处:在以后函数传参时可能传不同类型的值,使用const修饰能够更安全的将值传过去,并且不改变原来值得类型
指针和引用得关系
C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。
• 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
• 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
(也就是引用只能指向一个对象,而指针则可以指向多个对象)
• 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
• sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数
(32位平台下 占4个字节,64位下是8byte)
案例如下:
cpp
#include <iostream>
using namespace std;
int main()
{
char c = 'b';
char& rc = c;
char* pc = &rc;
cout << sizeof(c) << endl;
cout << sizeof(pc) << endl;
return 0;
}
打印结果如下:
• 指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
(野引用也有就是前面函数局部变量ret做返回值时的引用)
我们在学习时也要会看汇编代码,不会的可以去网上搜看
从底层汇编角度来看,引用也是用指针来实现的;从语法角度来看,引用是不创建空间的。
我们在看问题时要从多个角度来看,就像老婆饼里没有老婆,只是因为它做的香和脆像老婆做的一样。
inline函数
⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就需要建⽴栈帧了,就可以提⾼效率。
• inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适⽤于频繁
调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
• C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调
试,C语言设计了宏函数就是替代C的创建函数;C++设计了inline⽬的就是替代C的宏函数。
下面是相关的代码:
cpp
#include<iostream>
using namespace std;
// 实现⼀个ADD宏函数的常见问题
////#define ADD(int a, int b) return a + b;//err -- 宏定义不应包含 return语句,它应该展开为一个表达式。
//#define ADD(a, b) a + b;
#define ADD(a, b) (a + b)
// 正确的宏实现
//#define ADD(a, b) ((a) + (b))
int main()
{
int ret = ADD(1, 2);
// 为什么不能加分号?
cout << ret << endl;
//cout << ADD(1, 2) << endl;// err -- 这里在宏定义时加了;,在这里实际上是这样的:cout << a + b;; << endl;//带两个 ';' 分号
// 为什么要加外⾯的括号?
//cout << ADD(1, 2) * 5 << endl;//err --- 若不加,我们期望的是(1 + 2)* 5 = 15,没有加的话是 1 + 2 * 5 = 11,计算没有达到预期
// 为什么要加⾥⾯的括号?
int x = 1, y = 2;
ADD(x & y, x | y); // err -- 原本是((x&y)+(x|y)) 现在是 -> (x&y + x|y)
return 0;
}
cpp
#include<iostream>
using namespace std;
//使用了inline后不仅仅可以避免宏函数的坑,而且还不用创建函数栈帧,效率有所提高。
inline int add(int x, int y)
{
return x + y;
}
int main()
{
int ret = add(20, 10);
cout << ret << endl;
return 0;
}
虽然宏定义的缺点很多,但还是要使用它,原因是:使用它是时不用创建函数栈帧,提高代码运行效率
函数被编译以后是一堆指令,需要存储起来执行
第一句指令的地址就是函数的地址(每个编译器上各有所不同,在VS上的第一句指令不是函数的地址)
汇编也是可以调试
(先将代码用F10调试再单击右键点击反汇编即可查看),按F11,若观察名字有为call函数的,说明该内联函数inline没有起作用,总的来说就是下面这一点:
*
函数调用的本质就是:先使用call指令跳到一个地址,该指令是jmp指令地址,它会跳转到一个地址,再jmp一些就会跳转到一个地址,该地址是函数的地址
进入到该函数真实的地址后,下面是创建函数栈帧的一系列指令:
反汇编代码如下:


jmp两下到真实的地址

通过按F11观察到该反汇编代码有call,是为展开的,这是因为:
vs编译器若是短小的函数在release版本下默认展开,在debug版本下⾯默认是不展开inline的,这样⽅便调试,debug版本想展开需要设置⼀下以下两个地⽅:



修改完成后,在调试就可以看到inline内联函数展开的情况,如下图展开和未展开的情况对比:

inline对于编译器而言只是⼀个建议,加了inline编译器也可以选择在调用的地方不展开,展开的决策权在编译器手里,因为C++标准没有规定这个。 inline适⽤于频繁调用的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。
下面是调试的例子:add的展开取决于你的编译器,自己可以调试下看看展开和不展开的极限在哪里
cpp
#include<iostream>
using namespace std;
inline int Add(int x, int y)
{
int ret = x + y;
ret += 1;
ret += 1;
ret += 1;
ret += 1;
ret += 1;
return ret;
}
int main()
{
// 可以通过汇编观察程序是否展开
// 有call Add语句就是没有展开,没有就是展开了
int ret = Add(1, 2);
cout << Add(1, 2) * 5 << endl;
return 0;
}
想想为什么inline函数很长时会不展开,这里举个例子:
我们知道源代码通过编译链接生成的可执行文件后将一系列指令加载到进程内存中,假设我这里要调用1000次add函数,若展开则需要 10000 * 100 行代码;若不展开则需要10000 + 100行add函数的代码。
如果调用展开的,会让可执行文件变大,加载到进程中的内存会变大,这样做会很麻烦,所以inline会使用不展开的。就像玩的王者突然要跟新了,本来是500MB,结果一内联展开成1个GB了。
进程中一系列指令存储的具体位置在代码段中,栈是用来存储函数栈帧的。
所以决策权在编译器手里,发现代码太长了就不展开了,这是编译器的防御策略。
inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
cpp
//F.h
#pragma once
#include <iostream>
using namespace std;
inline void f(int i);
//F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
//test.cpp
#include "F.h"
int main()
{
// 链接错误:⽆法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z)
f(10);
return 0;
}
分别放到几个文件中后运行代码后会发现他会报错:

解决办法:直接将声明放到F.h文件中去
cpp
//F.h
#pragma once
#include <iostream>
using namespace std;
inline void f(int i)
{
cout << i << endl;
}
nullptr
NULL实际是⼀个宏,在C头⽂件中,NULL代表着((void *)0),但在C++中代表为 字符常量 ' 0 ',在C++中使用NULL会有一些麻烦,具体看代码:
cpp
#include<iostream>
using namespace std;
void f(int x)
{
cout << "f(int x)" << endl;
}
void f(int* ptr)
{
cout << "f(int* ptr)" << endl;
}
int main()
{
f(0);
f(NULL);
return 0;
}
结果如下:

NULL原本是指针,想要然它调用第二个,结果调用的都是第一个函数。即使是这样:
cpp
f((void*)NULL);
也会报错,因为在C++中不允许void*类型的随意变化,并且这是一个过时的写法不推荐。
使用nullptr就不会报错 ,用它来定义空指针可以避免类型转换的问题,因为nullptr只能被
隐式地转换为指针类型,⽽不能被转换为整数类型。
cpp
f(nullptr);
再次看打印结果:
