目录
💕1.C++中main函数内部———变量的访问顺序
💕2.命名空间域 namespace
💕3.命名空间域(代码示例)(不要跳)
💕4.多个命名空间域的内部重名
💕5.命名空间域的展开
💕6.展开的本质
💕7.输入输出流(IO)
💕8.缺省函数
💕9.函数的重载
💕10.引用的定义
💕11.引用的一些常见问题
💕12.引用——权限的放大/缩小/平移
💕13. 不存在的空引用
💕14.引用作为函数参数的速度之快(代码体现)
💕15.引用的思考
💕16.宏函数讲解
💕17.内联函数存在的意义是什么
💕18.如何书写内联函数
💕19.内联函数的意义与作用
💕20.内联函数注意事项
💕21.内联函数的展开条件 (如何设置)
💕22.关键字auto
💕23.关键字auto需要注意的点
💕24.auto遍历数组
💕25.C++中的NULL更迭
(最新更新时间——2025.1.12)
💕1.C++中main函数内部———变量的访问顺序
在C++中,main函数内部的变量的访问是有顺序存在的,其顺序分别为(从高到低):->
局部变量——全局变量——命名空间域
具体是什么意思?请看代码:
在局部变量与全局变量都存在时,main函数会优先访问局部变量,这也叫做局部优先原则
但如果不存在局部变量a=10呢?
当局部变量被去除后,main函数内部还是会遵循访问顺序来访问,但是它访问不到局部变量a,所以它就会去访问全局变量a
但这又是什么原因呢?有的人肯定会认为,这是因为rand是一个随机函数,创建rand变量就会与函数重名,但是这种原因是半对的,接着请看下图:->:
我把rand放在这里又可以打印出来了,这是为什么?
其实真正的原因是,在全局变量的rand与头文件#include<stdlib.h>是在同一作用域下,(即全局作用域),在同一作用域下的rand函数与rand变量命名重复,所以才会出现第一种报错的情况
关于第二种情况,则是作用域不同导致的,因为rand函数作用在全局作用域,rand变量作用在局部作用域,所以,作用域不同,它们的效果也不同
那么,有没有一种方法,可以解除第一种情况呢?答案是有的,请看一下讲解:->:
💕2.命名空间域 namespace
什么是命名空间域?
在C++中,每一个命名空间域就相当于一个田地,在这个田地中可以有各种东西,如变量,结构体,函数等等,而你可以给这个田地命名,让它变为独一无二的田地(命名作用域),
命名空间域的书写:
namespace 专属名 { //任意 }
我们以第一大点中错误的rand命名为例
如图所见,我们创建了命名空间域abc,在执行printf时,会优先在局部变量中找,找不到,再去全局变量,又找不到,最后去命名作用域找
注意:->我们在调用命名作用域时,运用到了 abc:: ,这表示访问命名作用域中的abc,当我们访问除了局部变量时的作用域,都可以这么表示,全局变量同理,如下图:
如图,在我们想用 ::访问全局变量时,需要注意的是::前面是有一位空格的
如果我们有多个重复的命名空间域,那这些命名空间域编译器会结合在一起的,请看代码->:
#include<iostream> #include<stdlib.h> using namespace std; namespace abc { int rand = 100; } namespace abc { int malloc = 200; } int main() { cout << abc::rand<<' '<<abc::malloc; //输出100 200 }
💕3.命名空间域(代码示例)(不要跳)
接下来是命名空间域的实际代码举例,包括(函数,结构体),以及命名作用域的嵌套
1.首先是 函数以及结构体 的调用方法
2.命名空间域的嵌套
💕4.多个命名空间域的内部重名
多个命名空间域可以在各自的内部重名吗?答案是可以的,因为每一个命名空间域都相当于一个菜园子,你的菜园子有变量A,我的菜园子就不能有了吗?那么代码举例如下:
如图可见,不同命名空间域的内部是可以重名的,但是每一个命名空间域是不可以重名的,除非使用命名空间域嵌套-给重名的命名空间域分开
💕5.命名空间域的展开
在我们写main函数时,每使用一次命名空间域都要写一次a::,是不是很麻烦,如图:
有没有一种方法,可以不这么麻烦呢?答案是有的兄弟,有的,像这么强的操作还有9....,
1种方法
那就是展开命名空间域
我们使用
using namespace 空间域名
来进行展开
如图,我们就不要再使用a::来访问了
但如果我们只想展开其中的一个呢?
代码如下:
using 空间域名::空间域变量
这里就单独展开了空间作用域a中的b,再举一个例子,单独展开空间作用域里的函数或者结构体,先是展开函数,后是展开结构体
💕6.展开的本质
在C++中,使用using展开的本质是将命名空间域展开到全局作用域中,举例如下:
这是不展开的情况:可以看到没有报错,逻辑也符合我们所讲的,作用于全局作用域
但如果我们展开了呢?
如图,就会发生错误,因为展开后的空间作用域相当于变为了全局作用域,而变量的访问顺序是局部,全局,空间作用域,展开后的全局作用域中有两个相同名称的函数,那么main函数内部就会不知道调用哪个add函数
💕7.输入输出流(IO)
在C++中,创建了新的输入和输出,它们的书写方式是:->
#include<iostream>
using namespace std;
int main()
{
int i = 0;
int j = 0;
cout << 20 << '\n' << 30 << '\n' << endl;//输出20,30 '\n'是换行
cin >> i >> j;//输入 i j
cout << i << " " << j << endl;//输出 i 空格 j
}
效果如下:
接下来是逐步讲解:
1.为什么要用using space std;
答案:->:因为#include<iostream>是一个标准头文件,而这个头文件,其实是一个巨大的空间域 ,因此在使用时,我们需要先展开空间域std,否则就需要写成:
#include<iostream> int main() { int i = 0; int j = 0; std::cout << 20 << '\n' << 30 << '\n' << std::endl;//输出20,30 '\n'是换行 std::cin >> i >> j;//输入 i j std::cout << i << " " << j << std::endl;//输出 i 空格 j }
2.cout是什么,cin是什么?
cout与cin都是空间域std里面的函数,cout表示输出除去,cin表示输入进来,当然如果各位不想用,printf与scanf也是可以正常使用在C++中的
3.endl是什么?
这里的endl,(注意不是end1,是英文L),endl在C++中表示的就是换行的意思
💕8.缺省函数
什么是缺省函数?
缺省函数是C++中的一种函数,它支持了你在给函数传参时不需要将所有参数都传过去,代码如下->:
我们发现,即使什么都没传过去,也依旧输出了200,而且我们在创建函数时,将形参初始化了
而这种每个函数在接收处都初始化的函数,叫做全省函数
有部分初始化的叫做半缺省函数,如下:
半缺省函数,需要从右往左缺省,不可以从左往右缺省,因为没有初始化的形参是必须传入值的,如果从左往右缺参,那就意味着我们不得不全部传参一遍
所以半缺省函数是要从右往左缺省的
那有的人问了,诶?那我跳着传不就可以了,但其实这是不可以的,代码如下:
答案显而易见,必须顺序传,不能跳跃着传
💕9.函数的重载
在C++中,是支持函数的名称重复的,但并不是完全重复
要求这些同名函数作用在同一作用域
且函数的形参不同->:可以是 参数类型不同 或者 参数个数不同 或者 参数顺序不同,这也叫做函数的重载
那么,函数的重载具体是什么呢?代码如下:
第一种情况:同名函数的形参类型不同——函数的重载
由代码可见,同名函数的形参是不同的,但是依旧可以运行,没有报错,我们也不必担心编译器会不知道运行哪个函数,因为在我们使用函数传参时,编译器会智能的去辨别类型,进而判断出该用哪个函数
第二种情况:同名函数的参数个数不同——函数的重载
由此可见,同名函数的参数的个数不同可以构成函数的重载
第三种情况:同名函数的形参类型顺序不同——函数的重载
需要注意的是,形参的顺序不同需要满足同名函数的形参类型不能完全相同,如下:
如果这么写的话,编译器也会不知道该调用哪个函数,这叫做调用歧义
注意点:返回值不可以作为函数重载的判断,因为调用时也无法区分
小练习->:
判断下面代码是否构成函数的重载
namespace bit1 { void total(int pa, int pb) { cout << "total(int pa, int pb)" << endl; } } namespace bit2 { void total(int px, int py) { cout << "total(int px, int py)" << endl; } } int main() { bit1::total(3, 5); bit2::total(8, 8); }
答案是否定的,因为两个同名函数并不作用于同一作用域下,所以并不构成函数的重载
💕10.引用的定义
在C++中,提出了“引用”的功能,什么是引用呢?
引用其实就是变量的别名
就相当于,你们班有一个学习特别好的人,数理化每次都是第一,那么这个同学就会获得三个别名,分别是“数学第一”,“物理第一”,“化学第一”,当你说数学第一时,大家知道你说的是他,当你说物理第一时,大家也知道你说的是他,这就叫做别名
引用书写方法->:
我们知道,学习C语言时, & 是取地址的意思,难道在C++中改变了吗?
其实没有,只有在你像图中这么使用时才会变为引用的意义
此时,b为a的别名,那么我们调用b的时候,其实就是调用a
问题:我们在创建别名时,是开辟了新的空间吗?
我们可以验证一下:
我们发现,b与a的地址一样,所以在引用时,并不会开辟新的空间,只会创建一个“别名”
💕11.引用的一些常见问题
各位可以思考一下,这种情况下a,b会交换吗?
#include<iostream> using namespace std; void Swap(int& ta, int& tb) { int tmp = ta; ta = tb; tb = tmp; } int main() { int a = 100; int b = 20; Swap(a, b); cout << a <<" " << b << endl; }
答案是会的,为什么?
我们先回到C语言时学习的交换,在C语言中,我们知道,如果不传地址的话,就无法改变两者的值,因为形参是实参的一份临时拷贝,swap函数会将a与b复制一份传给ta,tb,这时候会开辟新的地址空间,操作的是 ta 与 tb
但是这样却交换了,首先我们知道,除去变量名就是变量类型,我们在形参中的变量类型是int&类型,这种类型本身就是引用,是属于一个别名,所以我们将a传过去时,会将a的引用
传给形参,实际上发生的是int& ta = a,int& tb = b;
此时ta与tb就是别名,而上述我们提到过,引用是不会开辟新的空间的,所以实际上操作的就是a与b
注意小点1->:
在使用引用的过程中,我们也可以给别名引用,起出别名的别名,如下->:
注意小点2->:
在使用引用时,必须进行初始化,不可以说创建int&后就扔掉了,等一会有需要再用,代码如下:
注意小点3->:
一个变量可以有多个引用,但一个引用只可以绑定一个变量
#include<iostream> using namespace std; int main() { int a = 10; int& b = a; int& c = a; int l = 100; c = l; }
以上代码的含义是什么?
A.让l赋值给C?
B.让c成为l的别名?
答案是A,引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体,所以这里是赋值
💕12.引用——权限的放大/缩小/平移
在C++中,存在权限的放大,缩小,以及平移的问题,那么什么是权限的放大/缩小/平移呢?
请看代码->:
个人认为,权限的放大可以分为地址类与非地址类,首先讲解非地址类
非地址类->:
//非地址时 //权限的平移 int a1 = 10; int& b1 = a1; //权限的缩小 int a2 = 100; const int& b2 = a2; //权限的放大 const int a3 = 100; //int& b3 = a3; 这里是错误写法 //正确书写 const int& b3 = a3;
权限的平移(合法)->:
我们的变量a1是 int 类型的,而引用的b1也是 int 类型的,这说明类型的功能没有改变,属于权限的平移
权限的缩小(合法)->:
我们的a2是 int 类型的,而引用的b2是const int 类型的,我们知道,被const修饰后的变量,只可以读不可以修改,所以我们把一个既可以读又可以修改的变量,取了一个只可以读不可以修改的别名,这是属于权限的缩小,是合法的
权限的放大(非法)->:
我们的a3是const int 类型的,只可以读不可以修改,但是如果我们运行了错误的写法
(int& b3 = a3),那么就把只能读不能改的变量,取了一个即可以读又可以改的别名,这样的话,我们的b3是可以修改的,但是b3是a3的别名,我的本体不可以被修改,我的分身却可以修改我,这是权限的放大,是非法的
地址类->:
//地址情况 int a1 = 10; //权限的平移 int* p1 = &a1; int*& pp = p1; //权限的缩小 int* pp1 = &a1; const int* p2 = pp1; //权限的放大 const int* p3 = &a1; int* p4 = p3; int*& p4 = p3;
原理与上述相同,但需要注意的是,权限的缩小并没有运用到别名,只是单纯的赋值,这也说明,权限的放大/缩小/平移,并不只是单独存在于引用之中,同时我们还需要注意一下写法
//权限的缩小 int* pp1 = &a1; const int* p2 = pp1; const int*& p3 = pp1; // 错误的写法
为什么?因为它单纯触发了C++中const的安全保障,为了防治你用错间接修改上,所以禁止使用
思考->:
//这是权限的放大吗? int a1 = 10; int b1 = 20; const int& r1 = a1 + b1;
答案是,是的,因为a1+b1得到的值是可以修改的,而r1不可以修改,所以这也是权限的放大
💕13. 不存在的空引用
在我们使用引用时,是不可以实现空引用的,如下->:
但是有一种写法是合法的->:如下:
int* ptr = NULL; int& r = *ptr; cout << r << endl;
只不过打印不出来什么
💕14.引用作为函数参数的速度之快(代码体现)
运行以下代码,可以知道引用作为参数的运行快在哪里
#define _CRT_SECURE_NO_WARNINGS #include<iostream> using namespace std; struct A { int a[10000]; }; void TestFunc1(A a) {} void TestFunc2(A& a) {} void TestRefAndValue() { A a; // 以值作为函数参数 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 < 10000; ++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; }
我们可以发现,不以引用作为参数的话,那么在运行函数前就会重新开辟新的空间来作为临时变量,这需要消耗大量的栈佂与时间
而如果用引用作为参数的话,那么就不会开辟新的地址,因为实际运行的是A& a = i;这属于引用创建的别名,没有开辟新的空间,正如上述所讲,地址没有发生改变
💕15.引用的思考
int main() { // 权限可以平移/缩小 不能放大 double d = 12.34; // 类型转换 int i = d; int& r2 = d;//报错 const int& r1 = d;//不报错 //r1不能修改d return 0; }
我们知道,在 int i = d 时,因为类型的不同,所以它其实是有一个隐式类型转换的,此时,会将 d 复制一份并强制转化为 int类型 再赋值给 i ,所以 i 其实是复制 d 的一份 int 类型的值
其次当我们使用int& r2 = d时,因为d是double类型的,r2是int类型的,这里属于运算,还是会存在隐式类型转换,所以 r2 其实是 强制转化后的 d ,但是因为强制转化所生成的值是临时变量,临时变量是不可修改的,但是int& r2是可以修改的,这属于权限的放大,所以是不对的
那为什么我们使用const int& r1 = d 时,就不会报错呢?这里其实还是会因为类型的不同而存在强制类型转化,只要是运算就会存在强制类型转化,此时d还是会被复制一份并强制转化为int类型,而 r1 就是强制 int 类型的 d 的别名,但是被const修饰了,那就说明我们不可以对其进行修改值,属于权限的平移,也就不会报错,如果你想打印 r1 的话,你会发现打印出来的是 12
💕16.宏函数讲解
#define ADD(a, b) ((a) + (b)) int main() { int ret = ADD(1, 2); cout << ADD(1, 2) << endl; int x = 1, y = 2; ADD(x & y, x | y); // -> (x&y+x|y) return 0; }
思考->:为什么宏函数的 a 与 b 要各自有一个括号,这是因为 a 与 b实行的是a+b,而在实行a+b时,正如我们在main函数中写到的,ADD(x&y,x|y),因为&与|操作符的优先级小于+,所以如果不加括号,就会变成x&y+x|y,这里就会先算成y+x,然后算x&(y+x),最后算(x&(y+x))|y
💕17.内联函数存在的意义是什么
内联函数存在的意义
- 内联函数最初的目的:代替部分 #define 宏定义;
- 使用内联函数替代普通函数的目的:提高程序的运行效率;
💕18.如何书写内联函数
内联函数的书写方法->:
inline int Add(int a, int b) { int ret = a + b; return ret; } int main() { int c1 = Add(1, 2); cout << c1 << endl; return 0; }
我们可以发现,只要在我们之前所写函数的样例前,加上关键字 "inline" 就属于内联函数
那么,内联函数的作用是什么?
💕19.内联函数的意义与作用
内联函数,相当于把一整个函数展开,相当于将代码全部放在main函数内,这样省去了参数压栈、生成汇编语言的CALL调用、 返回参数、执行return等过程,从而提高了速度
我们举一个简单的例子,如下->:
inline int Add(int a, int b) { int ret = a + b; return ret; } int main() { int c1 = Add(1, 2); cout << c1 << endl; return 0; }
在这样的代码中,我们使用内联函数,省去了参数的空间开辟,省去了返回值的执行,更快地提高了运行的空间
但是这种情况并不是一直的,我们再举一个例子,如果说一个函数里有100行代码,而程序需要执行10000次,那么使用内联函数展开和不展开的区别是什么?
我们思考下,如果我们不展开的话,每一次执行一次函数,一共执行了10000次,每一次都会创建栈佂并进行销毁
而如果展开呢,每展开一次main函数内部会执行100行代码,那么展开10000次一共就执行了100W行代码,开辟内存空间会相当巨大,这就会导致同一个游戏,不展开时更新的内存极小,而展开后更新的内存极大
但是好在C++中避免了这种情况,在C++中,为了防止各位乱搞,C++祖师爷会在函数内部特别长的时候,即使你使用内联函数展开,也不会让你展开,这是一个极其好的点,示例如下->:
先来一段函数代码极长的情况->:
再由汇编码可见,即使我们使用了内联函数展开,依旧不会展开,因为函数内的代码太长了
(蓝色部分汇编码为调用函数——未展开)
接下来是函数内部代码少的时候->:
由汇编码可见,使用内联函数展开确实展开了函数在main函数内,并没有调用函数
💕20.内联函数注意事项
inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
我们可以从汇编码知道,调用函数是因为能找到函数的地址,而使用inline展开后,函数就不会开辟空间也就是不会有地址,那么没有地址的话自然就调用不了函数,这就导致在.h与.c文件中使用内联函数会出现一些问题,请看代码->:
我们发现,系统会报错,而这正是因为找不到main函数中找不到函数的地址,因为你展开到了abc.cpp中,因此函数的地址消失了,而main.cpp中就找不到了函数的地址,也就会编译错误,因此,在多文件时,我们都会将内联函数的声明和定义都写在头文件中
代码如下->:
abc.cpp中就不需要重复写了
💕21.内联函数的展开条件 (如何设置)
首先,内联函数只有在Release时才会实现,而在Debug时并不会实现,但是Release的情况下汇编码极其冗长复杂,那么有没有一种方法可以在Debug时也可以展开呢?
答案是有的,接下来按照我的操作->:
💕22.关键字auto
关键字auto会自动识别变量的类型,具体代码实例如下->:
int main() { auto a = 2; auto b = 2.2; auto c = 3.f; auto p = &a; cout << typeid(a).name() << endl;//打印int cout << typeid(b).name() << endl;//打印double cout << typeid(c).name() << endl;//打印float cout << typeid(p).name() << endl;//打印int* }
这里使用到了typeid().name,它会自动识别出变量的类型
💕23.关键字auto需要注意的点
在使用auto时,我们需要注意几点,如下->:
在auto后面时,如果我们什么都没加,那它就会自动识别为什么类型,但如果我们加了一些东西,那么我们传入的类型就必须是与之对应的
💕24.auto遍历数组
在C++中,我们想要遍历数组,可以使用除了for别的方法,那就是auto,具体代码如下->:
int main() { int arr[] = { 1,2,3,4,5,6,7,8,9 }; // 自动取数组arr中,赋值给k // 自动++,自动判断结束 for (auto& k : arr) { cout << k << " "; } }
这里的意思是指,自动取数组arr,将它赋值给它的别名-k,再自动++,并自动判断是否结束
💕25.C++中的NULL更迭
先看图片->:
在C语言中,我们通常指NULL就是空指针,但实际上,在C语言中的宏,NULL被定义为0,这也就导致了一个坑,当我们传参为NULL时,实际上会调用void f(int i)这种函数,直到C++中,这个坑被填平
在C++中,空指针被重新命名为 nullpter 因此在以后的C++中,空指针都会用nullptr表示,防止混淆