【C++入门】之填坑 C语言,详细原理讲解:命名空间、cout cin、缺省参数、函数重载、引用、const 权限问题、内联、auto、nullptr 和 NULL

写在前面:
		C++ 是在 C 语言基础之上加码完善而成的,在其学习之初,首要解决的就是 C 语言在编程上的明显缺陷。

一、命名空间(域)

C 语言 问题一:无法解决命名冲突的问题(自定义名和库冲突,项目之间的自定义名冲突),还引申出域的问题
C++ 提出解决:namespace,域作用限定符

C++ 有一个基本逻辑,同一个域下,不能出现相同命名

而创建各自的 命名空间 其实就是创建多个 。在各自的命名空间域下,便可以定义相同的变量名,编译器不会产生报错。

访问某个 命名空间 / 域 里面成员 的三种方式:

  1. 指定 命名空间访问 --> 空间名::成员
    ::(域作用限定符) 可以精准指向 指定域 中的 成员,空格默认为全局域。
  2. 部分 展开 --> using 空间名::对象名
    展开常用的命令。
  3. 全局 展开 --> using spacename 空间名
    相当于把命名空间又打开为全局,平时练习可以节省时间,但一般项目里,不推荐这样做。
//【局部域、全局域 的区别】主要:1.使用  2.生命周期

int a = 2;
void f1()
{
	int a = 0;
	printf("%d\n", a);	// 0  
	printf("%d\n", ::a);	// 2
}

int main()
{
	printf("%d\n", a);	// 2
	f1();
	return 0;
}

------
输出结果:
2
0 --> 局部优先于全局
2 --> :: 域作用限定符,空格符" "是默认的全局域,指挥编译器从全局中找

举例:两个程序员共同编写一个项目,变量或函数等命名相同的问题,就可以通过命名空间解决啦~

//【命名空间域】 只影响使用,不影响生命周期

/******************************** AQueue.h *******************************/
/*                              程序员小 A 实现的                          */
namespace AQueue     
{
	struct Node
	{
		struct Node* next;
		int val;
	};

	struct Queue
	{
		struct Node* head;
		struct Node* tail;
	};
}	// 注意:命名空间不加分号

/******************************** BList.h *******************************/
/*                              程序员小 B 实现的                         */
namespace BList
{
	struct Node
	{
		struct Node* next;
		struct Node* prev;
		int val;
	};
	struct List
	{
		struct Node* LNode;
		int sz;
	};
}

/******************************** Test.cpp *******************************/
#include "List.h"
#include "Queue.h"
using std::cout;	// 标准库 输出	(部分展开)
using std::endl;	// 标准库 换行	
using spacename AQueue;	//          (全部展开)
using spacename BList;


int main()
{
	// 指定空间访问
	struct AQueue::Node nodeQ;
	struct BList::Node nodeL;
	// 全局展开 -- 声明后可直接访问
	struct List l;
	struct Queue q;
	// 部分展开 -- 声明后可直接访问
	cout << "hello C++" << endl;
	
	return 0;
}

------
tips:命名空间是可嵌套的,如果命名空间也重名,里面再进行嵌套,加以区分
      相当于我在公安的居民信息系统里找一个人叫凯文,同省份的有十个个、同地级市的有三个,同小区就他一个,这样层层剖析就能精准定位到我要找的小凯文

注:std 是存放 C++ 标准库的空间名。


二、流插入、流提取 运算符

C 语言 问题二:输入输出的类型复杂繁多
C++ 提出解决:cout、cin,流插入、流提取 运算符

自动识别类型,但是也有控制精度等操作很麻烦。

C++ 的使用原则,兼容 C 语言,哪个方便用哪个


三、缺省参数

C 语言 问题三:函数设置了参数,则必须传参
C++ 提出解决:设置缺省参数,若调用函数未传参,则采用默认参数

函数定义、函数声明,只能 选择其中一个 设置缺省参数,同时给会报错。声明和定义如果分离,则只能在 声明 中设置。

缺省参数只能使用:全局变量、常量

比如:不能在类中,将缺省参数设置为类中定义的变量

缺省有 全缺省 和 半缺省。

// 【全缺省】
void func(int a = 1, int b = 2, int c = 3)
{
	cout << "a = " << a << " ";
	cout << "b = " << b << " ";
	cout << "c = " << c << " ";
	cout << endl;
}

int main()
{
	// 不能跳着传参
	func(11, 22, 33);
	func(44, 55);
	func(66);
	func();
	return 0;
}

------
输出结果:
a = 11 b = 22 c = 33
a = 44 b = 55 c = 3
a = 66 b = 2 c = 3
a = 1 b = 2 c = 3

如果是半缺省,使用缺省值,必须 从右往左 连续缺省

使用场景:在确定数据量的情况下,栈、堆 初始化时的扩容步骤,是非必要的消耗,使用缺省参数可以极大程度的避免该种消耗。

// 【半缺省】
// 场景复现~~
struct Stack
{
	int* a;
	int top;
	int capacity;
};

void StackInit(struct Stack* ps, int defualtCapacity = 4)
{
	ps->a = (int*)malloc(sizeof(int) * defualtCapacity);
	if (ps->a == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	ps->top = 0;
	ps->capacity = defualtCapacity;

	// 扩容部分实现...略
}
 
int main()
{
	struct Stack St1;	// 已知最大 100 个数据
	struct Stack St2;	// 不确定数据量
	StackInit(&St1, 100);	// 给出指定参数
	StackInit(&St2);	// 无法指定,则使用缺省参数
	return 0;
}

四、函数重载

C 语言 问题四:函数名称不允许相同,在一些实现相同,数据类型不同的情况下,只能设置多个名称不同的函数
C++ 提出解决:设置 函数重载

函数重载:同一个域里,允许定义同名函数,并由 形参列表(参数个数、参数类型、类型顺序) 来区分他们。值得注意的是,如果只是 返回值 不同,调用时是无法区分的,不能构成函数重载。

每个编译器都有自己的函数名修饰规则(由于 Windows 下 vs 的修饰规则过于复杂,而 Linux 下 g++ 的修饰规则简单易懂,笔者使用了 g++ 演示这个修饰后的名字),C++ 编译器 会将函数修饰成 【_Z + 函数长度 + 函数名 + 类型首字母】

上述针对描述的是在同一个域里,实际上联系第一点所讲,函数名、参数类型 和 名称空间 都被加入了修饰后名称,这样编译器(还有连接器)就可以区别同名但不同类型或名称空间的函数,而不会导致 link 的时候函数多重定义。

函数重载的所有的匹配,都是在编译时完成的(匹配可理解成,找到了该函数的编译地址),所以影响的是编译速度,并不会影响运行速度。

int Add(int a, int b)	// _Z3Addii
{
	return a + b;
}

double Add(double a, double b)	// _Z3Adddd
{
	return a + b;
}

int main()
{
	Add(1, 2);      // call  _Z3Addii(0x313131310)
	Add(1.1, 2.2);  // call  _Z3Adddd(0x313131320)
	return 0;
}

关于案例中 相似函数 重复定义的问题, 👉🔗 模板 👈 可以完美解决。


五、引用

C 语言 问题五:二级指针 + 实参形参,学 C 语言的永恒痛点
C++ 提出解决:引用 --> 形参的改变可以影响实参

引用:相当于给已经存在的变量取一个别名,即多个名字指的都是同一块空间,可以给别名取别名。

引用只能在定义时被初始化一次,之后别名绑定的对象不可变。

适用1:输出型参数

可以用在 C 语言中总是要传指针的地方,即我们希望通过形参的改变来直接改变实参。

int main()
{
	// 正常赋值
	int i = 0;
	int j = i;

	// 取别名
	int& k = i;
	int& m = k;	// 给别名取别名也 ok

	cout << &i << endl;
	cout << &j << endl;
	cout << &k << endl;
	cout << &m << endl;

	return 0;
}

------
输出结果:
00CFF744
00CFF738
00CFF744
00CFF744

使用场景 1_1

Swap() 交换函数,引用可以直接改变实参。

void Swap(int& x, int& y)
{
	int tmp = x;
	x = y;
	y = tmp;
}

int main()
{
	int i = 3;
	int j = 1;
	cout << "交换前:i = " << i << " j = " << j << endl;
	Swap(i, j);
	cout << "交换后:i = " << i << " j = " << j << endl;
	return 0;
}

------
输出结果:
交换前:i = 3 j = 1
交换后:i = 1 j = 3

使用场景 1_2

数据结构许多场景都可以使用。

//【1】
typedef struct Node
{
	struct Node* next;
	int val;
}Node;

void PushBack(Node*& phead, int x)
{
	Node* newNode = (Node*)malloc(sizeof(Node));
	if(phead == nullptr)
	{
		phead = newNode;
	}
	// 其余实现省略...
}

int main()
{
	Node* plist = NULL;
	PushBack(plist, 1);
	PushBack(plist, 2);
	return 0;
}
// 【2】
// 通过前序遍历的数组 “ABD##E#H##CF##G##” 构建二叉树
// BTNode* BinaryTreeCreate(char* a,int* pi)
BTNode* BinaryTreeCreate(char* a,int& ri)
{}

int main()
{
char* ptr = "ABD##E#H##CF##G##";
int i = 0;
BinaryTreeCreate(ptr, i);

return 0;
}

适用2:返回值

注意:函数返回时,出了函数作用域,如果返回对象还在(未被系统收回),则可以使用引用返回,如果还给系统了,必须使用传值返回。 可以使用的比如:静态、全局、上一层栈帧、malloc 的…

两个作用:
1. 减少了拷贝(提高了程序效率)
2. 调用者可以直接修改返回对象

使用场景2_1

static 静态变量

// 【传值返回】
// n 虽然储存在静态区,不随着函数栈帧的销毁而销毁
// 但在调用时,ret 接受的返回值,仍然不是 n 直接给它的,是在 main 栈帧里面提前开的一块空间放临时变量传递的(如果临时变量小,也可能是直接通过寄存器储存的)
int Count()
{
	static int n = 0;
	n++;

	return n;
}

// 【传引用返回】
// 这里的返回值,实际是通过 n 的别名也就是 n 的那块空间的值,减少了一次拷贝
// 相当于编译器把这一步的控制权交给了程序员
int& Count2()
{
	static int n = 0;
	n++;
	return n;
}

int main()
{
	int ret = Count();
	ret = Count2();
	return 0;
}

适用场景2_2

访问静态数组

/************************ 函数写法 ***************************/
#define N 10
typedef struct Array
{
	int a[N];
	int size;
}AY;

// 访问第 i 个数据
int& PosAt(AY& ay, int i)
{
	assert(i < N);
	return ay.a[i];	// 出了 PosAt 栈帧还在,所以可以用引用返回
}

int main()
{
	AY ay;
	for (int i = 0; i < N; i++)
	{
		PosAt(ay, i) = i * 10;	       
	}

	for (int i = 0; i < N; i++)
	{
		cout << PosAt(ay, i) << " ";  
	}

	return 0;
}
/********************** 结构体写法 *************************/
// 后面用类,会更加简洁
#define N 10
typedef struct Array
{
	int& at(int i)
	{
		assert(i < N);
		return a[i];
	}

	int a[N];
	int size;
}AY;

int main()
{
	AY ay;
	for (int i = 0; i < N; i++)
	{
		ay.at(i) = i * 10;	        // 修改返回对象
	}

	for (int i = 0; i < N; i++)
	{
		cout << ay.at(i) << " ";    // 访问
	}
	cout << endl;

	return 0;
}

一个错误案例:

int& Addtion(int a, int b)
{
	int c = a + b;
	return c;
}

int main()
{
	int& ret = Addtion(1, 2);
	Addtion(3, 4);
	cout << "Addtion(1, 2) is :" << ret << endl;
	cout << "Addtion(1, 2) is :" << ret << endl;
	return 0;
}

// 输出结果:
//Addtion(1, 2) is :7
//Addtion(1, 2) is :2043451784     --> 随机值

// 分析:
// add 函数中的 c 出了作用域都销毁了,引用他干嘛呀
// 语法上可以,不代表程序是对的,其结果无法定义~

const 权限问题

善用使用 const,可以极大程度保证被引用对象的值不被失误篡改

引用和指针一样,赋值 / 初始化 的权限可以 缩小持平,但是 不能放大

int CountN()
{
	int n = 0;
	n++;
	return n;
}

int main()
{
	/****************** 【权限放大】 err.. *****************/
	/*
	const int a = 2;
	int& b = a;

	const int* p1 = NULL;
	int* p2 = p1;
	*/
	/****************** 【权限平移 / 保持】 *****************/
	const int c = 2;
	const int& d = c;

	const int* p3 = NULL;
	const int* p4 = p3;

	const int& ret = CountN();	

	int i = 0;
	const double& rd = i;	// 只要类型转换会产生临时变量
	/********************* 【权限缩小】 *********************/
	int x = 1;
	const int& y = x;

	int* p5 = NULL;
	const int* p6 = p5;
	/*---------- 这只是单纯的赋值,不影响 m 的权限 -------------*/
	const int m = 1;
	int n = m;
 
	return 0;
}

补充

引用在语法的角度上来看,是不会开辟空间的。
但在汇编层面看,其实也是用指针实现的,具体汇编原理不是 C++ 学习重点,这里便不赘述。他较指针更为安全,使用也更加便捷。


六、内联

C 语言 问题六:宏缺点太多 --> 1.不能调试  2.没有类型安全的检查  3.有些场景下非常复杂。
		(宏函数更是一种暴力替换,可移植性极低)
C++ 提出解决:推荐使用 const 和 enum 替代宏常量,用 inline 去替代宏函数
		保留了宏的可维护性、不限制数据类型、不会开辟栈帧的优点

宏的回顾

// 宏函数 实际上就是暴力替换,多一个分号都会出岔子...
// 实现 ADD 宏函数(写时需谨慎)
// 两层括号,分别什么作用? --> 请深刻理解“暴力”替换有多“暴力”
#define ADD(x,y) ((x)+(y))

int main()
{
	ADD(1, 2) * 3;	// (1+2)*3	--> 外层大括号的作用

	int a = 1, b = 2;
	ADD(a | b, a & b);	// ((a | b) + (a & b)) --> 内层小括号的作用

	return 0;
}

inline

内联函数:以 inline 修饰 的函数,编译时 C++ 编译器在 调用内联函数的地方展开,不会有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

inline int Add(int x, int y)
{
	int z = x + y;
	return z;
}

int main()
{
	int ret = Add(1, 2);	// 编译器会在这里展开内联函数
	cout << ret << endl;
	return 0;
}

我们一般期望将 规模较小、不是递归、且频繁调用 的函数采用 inline 修饰,因为他是一种 用编译空间换时间 的方法,虽然可以提高程序的运行效率,但是可能会使目标文件变大。

打个比方:
一个程序有 1000 个地方调用了 Swap 函数,若 Swap 函数编译起来有10行,则
1. Swap 是普通函数
	Swap + 调用 Swap 指令,合集指令行 --> 1000 + 10
2. Swap 是 inline 函数
	Swap + 调用 Swap 指令,合集指令行 --> 1000 * 10

在实际编译中,内联只是向编译器发出的一个请求,编译器可以选择忽略这个请求…

inline 函数 的声明和定义

inline 函数的 声明和定义 ,需要 放在一起

先看现象:

/******************** Func.h **********************/
inline void f(int i);

/******************* Func.cpp *********************/
// 将函数定义与声明分开,放在这个文件中,会报错:error LNK2019 无法解析的外部符号,也就是链接错误
#include "Func.h"

inline void f(int i)
{
	cout << i << endl;
}

/******************* Test.cpp *********************/
#include "Func.h"
int main(){
	f(1);
	return 0;
}

------
修改建议:直接将 inline 函数在 .h 文件中定义即可

成因分析:

 正常来说:函数调用需要 call 一个地址,如果在展开的 Func.h 文件中,只有声明没有定义。
		编译器就会拿着编译后产生的函数名,去包含声明头文件的文件中找该函数,并完成调用。
		
 将内联函数声明定义分开在两个文件中,报错:链接错误 --> 链接不上,意思就是找不到

 原因:inline 函数是不进符号表的,他不会产生函数地址
 函数地址又是个啥?
 解释:是调用的时候 call 的一串符号,这串符号其实是一个用作编译跳转的地址,作用是转到编译 jmp 创建函数栈帧的地址
 
 inline 函数直接展开,根本不会开辟函数栈帧,所以也不会产生函数地址

七、auto

C 语言 问题七:赋值变量的时候,有时无法清楚的知道表达式的类型
C++ 提出解决:用 auto 声明变量,直接由编译器推导而得
int main()
{
	// 简单举例
	int a = 0;
	auto b = a;
	auto c = &a;
	cout << typeid(b).name() << endl;
	cout << typeid(c).name() << endl;
	return 0;

	// 复杂举例 (后期会讲到的 迭代器)
	std::map<std::string, std::string> dict;
	//std::map<std::string, std::string>::iterator dit = dict.begin();
	auto dit = dict.begin();	// 跟上一行作用一样
}

------
输出结果:
int
int *

typedef 是有缺陷的,这里使用 auto 更准确和简洁。

使用事项

auto 可以在一行定义多个变量,但这些变量必须是相同的类型,否则编译器会报错。因为编译器实际只对第一个类型进行推导,用推导出来的类型定义其余变量。

auto a = 1, b = 2;
auto c = 3, d = 4.0;	// err...编译失败

auto 不能做形参,不能声明数组

再体会一个 auto 的便捷用法

数组遍历:范围 for

自动依次取数组中的数据赋值给 e 变量,自动判断结束

	int arr[] = { 1,2,3,4,5,64,3,2 };
	
	/******************** 遍历的常规写法 ********************/
	for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	
	/******************** 范围 for 写法 ********************/
	/*----------- 打印 2 倍的数组,但是不改变原数组 -----------*/
	for (auto e : arr)	// 这里的 auto 也可以自己选择声明别的类型,e 是一个自定义的变量名,起什么都行
	{
		e *= 2;
		cout << e << " ";
	}
	cout << endl;
	/*--------------------- 查看原数组 --------------------*/
	for (auto e : arr)	
	{
		cout << e << " ";
	}
	cout << endl;
	
	/*------------------ 遍历改变原数组 --------------------*/
	for (auto& e : arr)	// 引用!!!
	{
		e *= 2;
	}
	/*--------------------- 查看原数组 --------------------*/
	for (auto e : arr)	
	{
		cout << e << " ";
	}
	cout << endl;
------
输出结果
2 4 6 8 10 128 6 4
1 2 3 4 5 64 3 2
2 4 6 8 10 128 6 4

八、nullptr 和 NULL

C 语言 问题七:NULL 在 C++ 中被定义成 0
C++ 提出解决:nullptr 

在使用 nullptr 表示空指针时,不需要包含头文件,因为他是 C++11 作为新关键字引入的。

在 C++11 中,ziseof(nullptr) 和 sizeof((void*)0) 所占字节数相同。

为了提高代码的健壮性,表示空指针时最好使用 nullptr

void f(int)
{
	cout << "f(int)" << endl;
}

void f(int*)
{
	cout << "f(int*)" << endl;
}

int main()
{
	f(0);
	f(NULL);	// c++ 里面的 NULL 被设置成了 0
	ff((int*)NULL);
	ff(nullptr);

	return 0;
}

------
输出结果:
f(int)
f(int)
f(int*)
f(int*)

🥰如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~


  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值