【C++程序员的自我修炼】基础语法篇(一)

心中若有桃花源

何处不是水云间


目录

命名空间

💞命名空间的定义

[💞 命名空间的使用](#💞 命名空间的使用)

输入输出流

缺省参数

函数的引用

引用的定义💞

引用的表示💞

引用的特性💞

常量引用💞

引用的使用场景

做参数

做返回值

⭐传值、传引用效率比较

⭐引用和指针的区别

命名空间

✨在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。

举个栗子:我想输出全局变量rand的值

cpp 复制代码
#include<stdlib.h>
#include<stdio.h>

int rand = 1;

int main()
{
	printf("%d\n", rand);
}

我们发现 rand 重定义了,因为 #include<stdio.h> 内有以rand 命名的函数,如果是以前我们为了解决这个问题还要憋屈的去改变量名,而现在 C++ 中的 命名空间就可以很好的解决问题。

使用命名空间的目的对标识符的名称进行本地化以避免命名冲突或名字污染

💞命名空间的定义

|-------------------------------------------------------------------------------------|
| ⭐namespace 是命名空间的关键字,Df 是命名空间的名字,一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中 |

cpp 复制代码
namespace Df
{
	int rand = 10;
}

|--------------------------------|
| ⭐命名空间可以嵌套(函数、结构体、另一个命名空间等) |

cpp 复制代码
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;
		}
	}
}

|---------------------------------------------|
| ⭐同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中 |

cpp 复制代码
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>加命名空间名称及作用域限定符,**::**是域限定符表示在该命名空间进行访问 |

cpp 复制代码
namespace Df
{
	int a = 0;
}
int main()
{
	printf("a = %d\n",Df::a);
	system("pause");
}

|--------------------------------|
| ✨<2>使用 using将命名空间中某个成员引入 |

cpp 复制代码
namespace Df
{
	int b = 1;
}
using Df::b;
int main()
{
	printf("b = %d\n", b);
	system("pause");
}

|---------------------------------------------|
| ✨<3>使用 using namespace 命名空间名称对该空间进行展开 |

cpp 复制代码
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>加命名空间名称及作用域限定符

cpp 复制代码
#include<iostream>
int main()
{
	std::cout << "Hello world !" << std::endl;
	return 0;
}

<2>使用 using 将命名空间中某个成员引入

cpp 复制代码
using std::cout;
using std::endl;
cout << "Hello world !" << endl;

<3>对全部命名空间进行展

cpp 复制代码
using namespace std;
cout << "Hello world !" << endl;

一些小细节💥

<1>编译默认查找顺序:当前局部域、全局域、命名空间中查找

举个栗子

如果把对变量的查找比作在地里采菜,那么命名空间就是一个访问权限问题

|------|----------|
| 局部域 | 自己家的地 |
| 全局域 | 外面的野地 |
| 命名空间 | 隔壁王叔叔家的地 |

假如某天家里缺辣椒了,自家地里没有,那就要去外面的野地去查找,若还是没有就看看隔壁王叔叔家的地有没有(如果有也不能随便采摘,因为没有经过王叔叔的同意),有权限后才可以采摘(域限定符进行申请访问)。


<2>对全部命名空间进行展开 using namespace std**(大型项目不宜使用,** 避免命名冲突 )


输入输出流

cout 标准输出流 和cin 标准输入流,包含于**#include<iostream>**头文件中

|----------------------------------------------------------------|
| 🌤️ >>是流插入与 cin 搭配使用,<<是流输出与 cout 搭配使用 |
| 🌤️ 输入输出流可以自动识别变量类型, 不需要像printf/scanf 输入输出时那样,需要手动控制格式 |
| 🌤️ coutcin 是全局的流对象,endl表示换行输出 |

cpp 复制代码
#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;
}

缺省参数

缺省参数是声明或定义函数时为函数的参数指定一个缺省值

在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参

cpp 复制代码
#include<iostream>
using std::cout;
using std::endl;
void Fun(int a = 10)
{
	cout << a << endl;
}

int main(void)
{
	Fun();//没有指定实参则采用缺省值
	Fun(314);
	return  0;
}

缺省参数的类型

|----------------|
| 🌤️全缺省:形参都是表达式 |

cpp 复制代码
#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>缺省值必须从左往右依次传,不可以间隔

|------------------|
| 🌤️半缺省:形参部分没有表达式 |

cpp 复制代码
#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>半缺省参数必须从右往左依次来给出,不能间隔着给

cpp 复制代码
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>缺省参数不能在函数声明和定义中同时出现

cpp 复制代码
#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;
}

函数的引用

引用的定义💞

引用不是新定义一个变量,而是给已存在变量取了一个别名

编译器不会为引用变量开辟内存空 间,它和它引用的变量共用同一块内存空间

引用的表示💞

|-------------------------------|
| 🌤️类型& 引用变量名(对象名) = 引用实体 |

cpp 复制代码
#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 取的别名,我们可以看到ab 以及地址都是一样的

比如水浒传的李逵,你叫黑旋风和铁牛他都会应,这就是引用(取别名)

注意💥

引用类型必须和引用实体是同种类型的

引用的特性💞

<1>引用在定义时必须初始化

<2>一个变量可以有多个引用

cpp 复制代码
#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>引用一旦引用一个实体,再不能引用其他实体

|-------------------------|
| 🌤️引用在定义时必须初始化 |
| 🌤️一个变量可以有多个引用 |
| 🌤️引用一旦引用一个实体,再不能引用其他实体 |

常量引用💞

cpp 复制代码
	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; int& ra = a; 对常量a进行引用,那么我就可以通过引用ra去修改a的值,权限放大了所以是不行的 |

|------------------------------------------------------------------------------|
| int c = 10; const int& rc = c; 对变量c进行引用,并将c置为不可修改的常量,权限的缩小所以是可行 |

|---------------------------------------------------------------------------|
| const int a = 10; const int& ra = a; 都是const加以修饰的同类型变量,权限的平移是可行的 |

总结

<1>权限只能缩小,不能放大,放大就会报错

<2>权限放大和缩小只针对引用和指针

<3>使用引用传参,函数内不改变参数,尽量使用 const 引用传参

引用的使用场景

做参数

💞<1>还记得我们之前写过的两数交换的代码吗?我们之前传的是指针 ,我们可以用引用代替

cpp 复制代码
#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>还记得我们之前写过链表时传的二级指针吗?今非昔比,我们来干爆他

前期回顾: 单链表

cpp 复制代码
typedef int SListDataType;

typedef struct SList
{
	SListDataType data;
	struct SList* next;
}SL;

void SListFront(SL** head, SListDataType x)
{
	...
}

这是我们的第一种写法,当初为啥要传二级指针呢?

因为我们没有定义哨兵位(头节点)而我们又想改变我们的首节点,假设我们传单指针SL*,SL*是结构体指针类型,这里是形参(形参是实参的拷贝,出了作用域就销毁),而我们要想改变首节点则必须传地址,所以我们要传二级指针

当然这是我们的一种写法,用引用该如何写呢?

cpp 复制代码
void SListFront(SL*& head, SListDataType x)
{
	...
}

如果你觉得别扭可以将结构体指针的声明放在typedef 中像这样

cpp 复制代码
typedef int SListDataType;

typedef struct SList
{
	SListDataType data;
	struct SList* next;
}SL*;

void SListFront(SL& head, SListDataType x)
{
	...
}

这样是不是好看多了!!!

做返回值

错误示范)·

cpp 复制代码
#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 一起销毁了,但是此时 retn 这块已经不属于自己的空间的拷贝,所以ret 是违法

那我们想二次调用 n 该怎么办呢?只要不销毁n 就可以了,我们可以给 n 开辟静态空间:

正确写法

cpp 复制代码
#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 修饰的局部变量,出了作用域会被销毁栈,必须用传值返回

引用作为函数的返回值时,必须在定义函数时在函数名前&

用引用作函数的返回值的最大的好处是在内存中不产生返回值的副本(寄存器)

我们在举一个栗子~

cpp 复制代码
#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时,ret3 ,Add函数的栈桢销毁,在第二次调用时,Add函数的栈桢是相同的,c 的位置覆盖为7 ,再次访问ret 此时就为 7,因此这里使用是不安全的

总结💥

如果函数返回时,出了函数作用域,如果返回对象还在,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回


传值、传引用效率比较

cpp 复制代码
#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;
}

我们发现:引用作为返回值类型大大提高了效率

原因:以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

引用和指针的区别

引用在语法概念上引用就是一个别名,没有独立空间,指针在底层实现上实际是有空间的

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	//指针存储a的地址
	int* pa = &a;
	//b是a的引用
	int& b = a;
	return 0;
}

没有NULL引用,但有NULL指针

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
	int* a = NULL;
	int& b = a;
	return 0;
}

在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数

cpp 复制代码
#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,指针自加即指针向后偏移一个类型的大小

cpp 复制代码
#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,指针自加即指针向后偏移一个类型的大小 |
| 🌤️有多级指针 ,但是没有多级引用 |
| 🌤️访问实体方式不同,指针 需要显式解引用,引用编译器自己处理 |
| 🌤️引用指针使用起来相对更安全 |

先介绍到这里啦~

有不对的地方请指出💞

相关推荐
吕小明么23 分钟前
OpenAI o3 “震撼” 发布后回归技术本身的审视与进一步思考
人工智能·深度学习·算法·aigc·agi
大胆飞猪37 分钟前
C++9--前置++和后置++重载,const,日期类的实现(对前几篇知识点的应用)
c++
1 9 J40 分钟前
数据结构 C/C++(实验五:图)
c语言·数据结构·c++·学习·算法
程序员shen16161142 分钟前
抖音短视频saas矩阵源码系统开发所需掌握的技术
java·前端·数据库·python·算法
夕泠爱吃糖42 分钟前
C++中如何实现序列化和反序列化?
服务器·数据库·c++
长潇若雪1 小时前
《类和对象:基础原理全解析(上篇)》
开发语言·c++·经验分享·类和对象
汝即来归1 小时前
选择排序和冒泡排序;MySQL架构
数据结构·算法·排序算法
咒法师无翅鱼1 小时前
【定理证明工具调研】Coq, Isabelle and Lean.
算法
风清云淡_A2 小时前
【java基础系列】实现数字的首位交换算法
java·算法
涵涵子RUSH2 小时前
合并K个升序链表(最优解)
算法·leetcode