C++入门基础(下)

0 阅读7分钟
前言

本文承接前一篇文章,把C++入门基础全部讲解完成

1.引用

C++的很多语法是为了替代些比较麻烦或者容易出错的语法,引用就是其中之一。

<1>引用的概念和定义

引用不是新定义一个变量,而是给已经存在的变量取一个别名,编译器不会为引用变量单独开辟一块内存空间,而是与其引用的变量共用同一块内存空间,格式是:类型& 引用别名=引用对象;
下面作者给大家举个例子:

int main()
{
		int a = 0;
		// 引用:b和c是a的别名
		int& b = a;
		int& c = a;
	
		// 也可以给别名b取别名,d相当于还是a的别名
		int& d = b;
		++d;

		// 这里取地址我们看到是一样的
		cout << &a << endl;
		cout << &b << endl;
		cout << &c << endl;
		cout << &d << endl;
	return 0;
}

运行后可以看到结果是这样的:

image.png
因此我们不难验证上述结论.由于引用的这个性质,其实我们可以通过一个简单的交换函数看出引用较指针的一些优越性:

void Swap(int& rx, int& ry)
{
	int tmp = rx;
	rx = ry;
	ry = tmp;
}

void Swap(int* px, int* py)
{
}
int main()
{
		int x = 0, y = 1;
		cout << x << " " << y << endl;
		//Swap(&x, &y);
	
		Swap(x, y);
		cout << x << " " << y << endl;

这个交换函数比较简单,如果用指针完成这个函数,显然是不如引用更直接方便的。

<2>引用的特性

引用有下面三个特性:

  • 引用在定义时必须被初始化
int a = 10;
int& ra;

比如这段代码,由于ra并没有初始化,因此一定会报错

  • 一个变量可以有多个引用
	int& b = a;
	int& c = b;

比如这段代码,其实b,c都是a的别名,也就是a具有多个引用。

  • 引用一旦引用一个实体,就不能引用其他实体
int d = 20;
b = d;

比如这里的b,d,d不是b的引用,这里只是一个赋值关系,引用的指向是不能改变的(这也是引用无法替代指针的原因)。

<3>引用的使用

在真正学习引用的使用之前,希望读者能想起前面学过的知,在C语言的学习中我们知道传值传参本质是先拷贝一份再传过去,下面我们就可以开始学习引用使用

  • 引用在实践中主要用于引用传参和引用左返回值中减少拷贝提高效率
  • 引用传参和指针传参功能类似但是引用传参更加方便
  • 引用和指针相辅相成不能完全替代,最重要的区别是指针可以改变指向,引用不可以 下面作者展示一个案例,通过这个案例来细节引用的使用:
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)
{
	//assert(ps);

	// 满了, 扩容
	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)
int& STTop(ST& rs)
{
	assert(rs.top > 0);
	return rs.a[rs.top];
}
int main()
{
	// 调用全局的
	ST st1;
	STInit(st1);
	STPush(st1, 1);
	STPush(st1, 2);

	cout << STTop(st1) << endl;

	return 0;
}

上面的代码实现了一个栈以及一些栈的功能,通过上述代码不难看出引用的优势,因为指针会有空指针的情况,不过指针和引用通常是一起用的,比如下面这段代码:

typedef struct ListNode
{
	int val;
	struct ListNode* next;
}LTNode, * PNode;
// {}*PNode   等价于  typedef struct ListNode* PNode;

//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;
	LTNode* plist = NULL;
	ListPushBack(plist, 1);
	return 0;
}

上面代码定义了一个链表,在C语言实现链表操作时,需要用指针的指针来调整节点的操作,但是有引用时就可以避免多用一重指针,但是不存在&&这种重引用。

<4>const引用

const引用其实主要是涉及到权限问题,简而言之只有一句话:权限可以放大,不能缩小。

  • 引用const对象必须用const引用,const引用也可以引用普通对象 其实很好理解,const修饰一个对象,如果不用const引用,那么别名是可以随意修改的,引用对象也就可以随意修改了,这显然是矛盾的,这也就是权限放大缩小的问题。
  • 注意临时对象具有常性
    临时对象在很多情况下都会产生:函数传值返回,运算符产生的新值,类型转换时,函数传参时按值传参,根本原因是从一个已有对象创建一个新的独立对象时,会涉及到拷贝,也就是说有一个中间的变量,也就是临时变量,读者务必记住,临时变量具有常量,也就是需要用const引用才行,下面作者展示一些情况:
//辨析临时对象具有常性
int main()
{
	const int a = 10;
	const int& ra = a;//权限不可放大
	int b = 20;
	const int& rb = b;//权限可以缩小
	b++;
	//rb++;  报错
	//int rd=a;//可读可写,只是一个拷贝,不存在权限问题
	const int& rc = 30;//const引用可以给常量取别名
	const int& rd= (a + b);//临时对象具有常性;
	//int rd = (a + b);//正确
	double d = 12.34;
	int i = d;
	const int& ri = d;//ri不能直接引用d
	return 0;
}

<5>指针和引用的关系

其实通过上文的学习,我们已经很好掌握了指针和引用的区别,指针改变指向,引用不允许改变,指针需要解引用,而引用直接访问即可,引用不会像指针那样容易出现野指针,空指针,二者谁都不能完全替代谁。

int main()
{
	int a = 0;
	int* p = &a;
	*p = 1;
	int& ra = a;
	ra = 2;

	// 空引用
	int* ptr = NULL;
	int& rb = *ptr;
	rb++;
	const int& rc = a + 2;
	return 0;
}

2.inline

<1>宏定义函数

inline其实是对标的C语言函数中的宏定义,首先我们先回忆一下宏定义函数,宏定义函数本质是函数,不过有诸多问题:

//inline     
// 实现一个ADD宏函数的常见问题
//#define ADD(int a, int b) return a + b;
//#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); // int ret = ((1)+(2));
	cout << ADD(1, 2) << endl;

	cout << ADD(1, 2) * 5 << endl;

	int x = 1, y = 2;
	ADD(x & y, x | y); // -> (x&y+x|y)

	return 0;
}

比如上述代码实现的ADD函数,宏定义的函数有很多限制而且不易于调试,因此才会引出inline内联函数

<2>内联函数

用inline修饰的函数叫内联函数,编译时C++编译器会在调用的地方展开,展开之后因为调用函数就会建立栈帧,这样就可以提高效率。
注意:如果是一个递归函数,且递归过深时,那么展开这个内联函数会直接出问题,所以这种情况不要展开。
inline在编译器中仅仅是一个建议,编译器不一定采纳。

inline int Add(int x, int y)
{
	int ret = x + y;
	ret += 1;
	ret += 1;
	ret += 1;
	ret += 1;
	ret += 1;
	ret += 1;
	ret += 1;
	ret += 1;
	ret += 1;
	return ret;
}
int main()
{
	int ret = Add(1, 2);
	cout << Add(1, 2) * 5 << endl;
	cout << ret << endl;
	f(10);
	return 0;
}

如果读者对内联函数的底层感兴趣,那么可以转到反汇编观察底层行为。

3.nullptr

<1>NULL

首先回忆一下C语言中的NULL,NULL本质上是一个宏:

#ifndef NULL

#ifdef __cplusplus

#define NULL 0

#else

#define NULL ((void *)0)

#endif

#endif

上面就是在C语言L的定义,因此C语言其实是把NULL宏定义成了0,但是如果遇到void*时就会出问题:
f((void*)0); C++不允许void*指针给到其他类型,只能强转
因此提出了nullptr。

<2>nullptr

C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。

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

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

int main()
{
	f(0);
	f(NULL);
	//f((void*)0); C++不允许void*指针给到其他类型,只能强转

	f(nullptr);

	// C++
	void* p1 = NULL;
	int* p2 = (int*)p1;

	int* p2 = nullptr;
	//int i = nullptr; 

	return 0;
}
结语

C++入门基础就此结束,看似学习这些并无实际用处,但是在后面这些知识会发挥重要作用,下面作者将更新类和对象,希望读者持续关注。