心中若有桃花源
何处不是水云间
目录
命名空间
💞命名空间的定义
💞 命名空间的使用
输入输出流
缺省参数
函数的引用
引用的定义💞
引用的表示💞
引用的特性💞
常量引用💞
引用的使用场景
做参数
做返回值
⭐传值、传引用效率比较
⭐引用和指针的区别
命名空间
✨在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。
举个栗子:我想输出全局变量 rand 的值
#include<stdlib.h>
#include<stdio.h>
int rand = 1;
int main()
{
printf("%d\n", rand);
}
我们发现 rand 重定义了,因为 #include<stdio.h> 内有以 rand 命名的函数,如果是以前我们为了解决这个问题还要憋屈的去改变量名,而现在 C++ 中的 命名空间 就可以很好的解决问题。
使用命名空间的目的是对标识符的名称进行本地化, 以避免命名冲突或名字污染
💞命名空间的定义
⭐namespace是命名空间的关键字,Df是命名空间的名字,一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中 |
namespace Df
{
int rand = 10;
}
⭐命名空间可以嵌套(函数、结构体、另一个命名空间等) |
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N2
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
⭐同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中 |
namespace N1
{
int a;
int b;
int Add(int left, int right)
{
return left + right;
}
namespace N2
{
int c;
int d;
int Sub(int left, int right)
{
return left - right;
}
}
}
namespace N1
{
int Mul(int left, int right)
{
return left * right;
}
}
💞 命名空间的使用
✨<1>加命名空间名称及作用域限定符,:: 是域限定符表示在该命名空间进行访问 |
namespace Df
{
int a = 0;
}
int main()
{
printf("a = %d\n",Df::a);
system("pause");
}
✨<2>使用 using 将命名空间中某个成员引入 |
namespace Df
{
int b = 1;
}
using Df::b;
int main()
{
printf("b = %d\n", b);
system("pause");
}
✨<3>使用 using namespace 命名空间名称对该空间进行展开 |
namespace Df
{
int a = 0;
int b = 1;
}
using namespace Df;
int main()
{
printf("a = %d\n", a);
printf("b = %d\n", b);
system("pause");
}
日常操作🔥
c++库为了防止命名冲突,就把库里面的东西都定义在一个 std 的命名空间里,所以要使用c++库中的函数方法与上述三种方法一样
<1>加命名空间名称及作用域限定符
#include<iostream>
int main()
{
std::cout << "Hello world !" << std::endl;
return 0;
}
<2>使用 using 将命名空间中某个成员引入
using std::cout;
using std::endl;
cout << "Hello world !" << endl;
<3>对全部命名空间进行展
using namespace std;
cout << "Hello world !" << endl;
一些小细节💥
<1>编译默认查找顺序:当前局部域、全局域、命名空间中查找
举个栗子
如果把对变量的查找比作在地里采菜,那么命名空间就是一个访问权限问题
假如某天家里缺辣椒了,自家地里没有,那就要去外面的野地去查找,若还是没有就看看隔壁王叔叔家的地有没有(如果有也不能随便采摘,因为没有经过王叔叔的同意),有权限后才可以采摘(域限定符进行申请访问)。
局部域 自己家的地 全局域 外面的野地 命名空间 隔壁王叔叔家的地
<2>对全部命名空间进行展开 using namespace std (大型项目不宜使用,避免命名冲突)
输入输出流
cout 标准输出流 和 cin 标准输入流,包含于 #include<iostream> 头文件中
🌤️ >> 是流插入与 cin 搭配使用,<<是流输出与 cout 搭配使用 🌤️ 输入输出流可以自动识别变量类型,不需要像 printf/scanf 输入输出时那样,需要手动控制格式 🌤️ cout 和 cin 是全局的流对象,endl 表示换行输出 #include<iostream> using std::cout; using std::cin; using std::endl; int main() { int a; double b; cin >> a >> b; cout << a << b << endl; return 0; }
缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个缺省值
在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参
#include<iostream> using std::cout; using std::endl; void Fun(int a = 10) { cout << a << endl; } int main(void) { Fun();//没有指定实参则采用缺省值 Fun(314); return 0; }
缺省参数的类型
🌤️全缺省:形参都是表达式 #include<iostream> using std::cout; using std::endl; void Fun(int a = 10,int b = 20,int c = 30) { cout << a << endl; cout << b << endl; cout << c << endl; } int main(void) { Fun(1); return 0; }
注意:
<1>缺省参数不是要全部传完,没有指定实参采用该形参的缺省值
<2>缺省值必须从左往右依次传,不可以间隔
🌤️半缺省:形参部分没有表达式 #include<iostream> using std::cout; using std::endl; void Fun(int a,int b = 20,int c = 30) { cout << a << endl; cout << b << endl; cout << c << endl; } int main(void) { Fun(1); return 0; }
注意:
<1>半缺省参数必须从右往左依次来给出,不能间隔着给
void Fun(int a = 10,int b = 20,int c) { cout << a << endl; cout << b << endl; cout << c << endl; } int main(void) { Fun(1); return 0; }
因为第三个形参 c 不是表达式,没有缺省值,而我们实参传的数值是传给 a 的所以报错<2>缺省参数不能在函数声明和定义中同时出现
#include<iostream> using std::cout; using std::endl; void Fun(int a = 10, int b = 20, int c = 30); int main() { Fun(1); return 0; } void Fun(int a = 10, int b = 20, int c = 30) { cout << a << endl; cout << b << endl; cout << c << endl; }
函数的引用
引用的定义💞
引用不是新定义一个变量,而是给已存在变量取了一个别名
编译器不会为引用变量开辟内存空 间,它和它引用的变量共用同一块内存空间
引用的表示💞
🌤️类型& 引用变量名(对象名) = 引用实体 |
#include<iostream>
using std::cout;
using std::endl;
int main()
{
int a = 10;
int& b = a;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "&a = " << &a << endl;
cout << "&b = " << &b << endl;
return 0;
}
因为 b 是给 a 取的别名,我们可以看到 a 和 b 的值以及地址都是一样的
比如水浒传的李逵,你叫黑旋风和铁牛他都会应,这就是引用(取别名)
注意💥
引用类型必须和引用实体是同种类型的
引用的特性💞
<1>引用在定义时必须初始化
<2>一个变量可以有多个引用
#include<iostream>
using std::cout;
using std::endl;
int main()
{
int a = 10;
int& b = a;
int& c = b;
cout << a << endl << b << endl << c;
return 0;
}
<3>引用一旦引用一个实体,再不能引用其他实体
🌤️引用在定义时必须初始化 |
🌤️一个变量可以有多个引用 |
🌤️引用一旦引用一个实体,再不能引用其他实体 |
常量引用💞
const int a = 10;
//int& ra = a; //该语句编译时会出错,a为常量
const int& ra = a;
double b = 3.14;
//int& rb = b; //该语句编译时会出错,类型不同
double& rb = b;
int c = 10;
const int& rc = c;
加了 const 该变量是不能修改的,即成了一种常量,可以理解为只读型
而没加 const 的可理解为可读可写型
const int a = 10; 对常量a进行引用,那么我就可以通过引用ra去修改a的值,权限放大了所以是不行的 |
int c = 10; const int& rc = c; 对变量c进行引用,并将c置为不可修改的常量,权限的缩小所以是可行 |
const int a = 10; 都是const加以修饰的同类型变量,权限的平移是可行的 |
总结
<1>权限只能缩小,不能放大,放大就会报错
<2>权限放大和缩小只针对引用和指针
<3>使用引用传参,函数内不改变参数,尽量使用 const 引用传参
引用的使用场景
做参数
💞<1>还记得我们之前写过的两数交换的代码吗?我们之前传的是指针,我们可以用引用代替
#include<iostream>
using std::cout;
using std::endl;
void Swap(int& n, int& m)
{
int tmp = n;
n = m;
m = tmp;
}
int main()
{
int a = 10, b = 20;
Swap(a, b);
cout << a << endl << b << endl;
return 0;
}
💞<2>还记得我们之前写过链表时传的二级指针吗?今非昔比,我们来干爆他
前期回顾:单链表
typedef int SListDataType;
typedef struct SList
{
SListDataType data;
struct SList* next;
}SL;
void SListFront(SL** head, SListDataType x)
{
...
}
这是我们的第一种写法,当初为啥要传二级指针呢?
因为我们没有定义哨兵位(头节点)而我们又想改变我们的首节点,假设我们传单指针SL*,SL*是结构体指针类型,这里是形参(形参是实参的拷贝,出了作用域就销毁),而我们要想改变首节点则必须传地址,所以我们要传二级指针
当然这是我们的一种写法,用引用该如何写呢?
void SListFront(SL*& head, SListDataType x)
{
...
}
如果你觉得别扭可以将结构体指针的声明放在 typedef 中像这样
typedef int SListDataType;
typedef struct SList
{
SListDataType data;
struct SList* next;
}SL*;
void SListFront(SL& head, SListDataType x)
{
...
}
这样是不是好看多了!!!
做返回值
(错误示范)·
#include<iostream>
using namespace std;
int& Count()
{
int n = 10;
return n;
}
int main()
{
int& ret = Count();
cout << "ret = " << ret << endl;
cout << "ret = " << ret << endl;
return 0;
}
让我们来分析分析,以上打印的值都是 10 吗?
我们发现我们打印的第二个 ret 竟然是随机值,这是为什么呢?
因为局部变量存储在系统的栈区,出了定义域就会销毁
🌤️第一次打印原值是因为编译器在释放时会进行一次保留
Count 函数并不是直接返回 n 的
因为 Count 函数在调用结束后会销毁它所在的栈帧,连同 n 会一起销毁,所以编译器会先保存 n的值到一个寄存器中,再销毁栈帧,然后返回寄存器的值给 ret
🌤️第二次出现乱码是因为赋值后寄存器的空间被编译器销毁
当我们用上面的代码,返回的是 n 的引用时,这就不安全了。因为返回的是 n 的引用,不会创建临时空间给 n,而是直接返回 n 。 但是返回之后 n 所在的函数栈帧会被销毁,所以连同 n 一起销毁了,但是此时 ret 是 n 这块已经不属于自己的空间的拷贝,所以 ret 是违法
那我们想二次调用 n 该怎么办呢?只要不销毁 n 就可以了,我们可以给 n 开辟静态空间:
(正确写法)
#include<iostream>
using namespace std;
int& Count()
{
static int n = 10;
return n;
}
int main()
{
int& ret = Count();
cout << "ret = " << ret << endl;
cout << "ret = " << ret << endl;
return 0;
}
此时 n 是被 static 修饰过的变量,可以用引用进行返回了,因为 n 是在静态区开辟的空间在内存的堆区,而函数是在栈区开辟的空间,所以不会被销毁。其次因为在堆区返回的时候就不需要借助寄存器的临时拷贝了。
⭐局部变量用 static 修饰,出作用域后不销毁,可以传引用返回
⭐没有用 static 修饰的局部变量,出了作用域会被销毁栈,必须用传值返回
⭐引用作为函数的返回值时,必须在定义函数时在函数名前&
⭐用引用作函数的返回值的最大的好处是在内存中不产生返回值的副本(寄存器)
我们在举一个栗子~
#include<iostream>
using namespace std;
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :" << ret << endl;
return 0;
}
这里为什么输出的是 7 呢?
就像我们学指针时遇到的野指针,在我们 free 掉不用的空间后没有将指针置为NULL,此时该指针还指向这块空间,但是值却是随机的
因为 c 是在栈区在第一次调用Add时,ret 为 3,Add函数的栈桢销毁,在第二次调用时,Add函数的栈桢是相同的,c 的位置覆盖为 7,再次访问 ret 此时就为 7,因此这里使用是不安全的
总结💥
如果函数返回时,出了函数作用域,如果返回对象还在,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回
⭐传值、传引用效率比较
#include <time.h>
#include<iostream>
using namespace std;
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
我们发现:引用作为返回值类型大大提高了效率
原因:以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
⭐引用和指针的区别
✨引用在语法概念上引用就是一个别名,没有独立空间,指针在底层实现上实际是有空间的
#include<iostream>
using namespace std;
int main()
{
int a = 10;
//指针存储a的地址
int* pa = &a;
//b是a的引用
int& b = a;
return 0;
}
✨ 没有NULL引用,但有NULL指针
#include<iostream>
using namespace std;
int main()
{
int* a = NULL;
int& b = a;
return 0;
}
✨ 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数
#include<iostream>
using namespace std;
int main()
{
double a = 10;
double* b = &a; //指针取地址
cout << sizeof(b) << endl;
double& c = a; //引用
cout << sizeof(c) << endl;
return 0;
}
✨ 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
#include<iostream>
using namespace std;
int main()
{
double a = 10;
double* b = &a;
cout << b << endl;
b++;
cout << b << endl;
double& c = a;
cout << c << endl;
c++;
cout << c << endl;
return 0;
}
总结 💥
🌤️引用在语法概念上引用就是一个别名,没有独立空间,指针在底层实现上实际是有空间的 |
🌤️引用在定义时必须初始化,指针没有要求 |
🌤️引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何 一个同类型实体 |
🌤️没有NULL引用,但有NULL指针 |
🌤️在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数 |
🌤️引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小 |
🌤️有多级指针,但是没有多级引用 |
🌤️访问实体方式不同,指针需要显式解引用,引用编译器自己处理 |
🌤️引用比指针使用起来相对更安全 |
先介绍到这里啦~
有不对的地方请指出💞