C++入门指南(下篇)

   各位小伙伴们好!在 【C++入门指南(上篇) 】中我们详细讲解了命名空间、输入输出、缺省参数、函数重载等知识,本篇文章我们将继续学习C++中的基础知识,那么话不多说,系好安全带,我们发车啦!


1. 引用

1.1 什么是引用?

   引用就是给一个已经存在的变量起一个别名。引用本身不占用额外内存(从概念上讲),它和原变量共享同一块内存空间。对引用的任何操作,本质上都是在操作原变量

定义引用的语法:
类型& 引用别名 = 引用对象

举个例子:

#include<iostream>
using namespace std;

int main()
{
	int a = 1;
	//这里的b和c是a的别名
	int& b = a;
	int& c = a;
	//这里给b取别名,d相当于还是a的别名
	int& d = b;

	//如果打印它们的地址的的话,你会神奇的发现
	//它们是一样的!
	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
	cout << &d << endl;

	return 0;
}

效果:
在这里插入图片描述

1.2 引用的特性

1. 必须初始化

   引用在定义时必须绑定到一个变量,不能只声明不初始化.

int &c;        // 错误!引用必须初始化
int &d = a;    // 正确

2. 一旦绑定,不能改变

   引用从一而终,一旦绑定到某个变量,就不能再改绑到其他变量

int x = 5, y = 10;
int &r = x;
r = y;       // 这不是让 r 绑定到 y,而是把 y 的值赋给 x(x 变成 10)
// r 仍然是 x 的引用

3. 没有独立的内存地址

   从语言层面讲,引用不是对象(指针是对象),它不占用存储空间(但底层实现可能用指针,这是编译器的事)。
   如果你试图获取引用的地址,得到的是原变量的地址。

1.3 引用的使用场景

1. 作为函数参数(最常用)

   引用最经典的用途是做函数参数,实现传引用调用,避免拷贝,同时可以在函数内部修改实参。
举个例子:

#include<iostream>
using namespace std;

void Swap(int& rx, int& ry)//rx是x的别名,ry是y的别名
{
	int tmp = rx;
	rx = ry;
	ry = tmp;
}
int main()
{
	int x = 1, y = 2;
	cout << x << " " << y << endl;

	Swap(x, y);//交换
	cout << x << " " << y << endl;
	return 0;
}

效果:
在这里插入图片描述

2. 作为函数返回值

   引用可以作为函数的返回值,这样可以实现链式调用,或者让函数返回的对象可以修改。

int arr[] = {1, 2, 3};
int& getElement(int x) 
{
    return arr[x];   // 返回引用,意味着可以修改数组元素
}
int main() 
{
    getElement(1) = 100;  // arr[1] 变成 100
}

注意:不要返回局部变量的引用,因为局部变量在函数结束时会销毁,返回的引用就成了“悬空引用”,使用会导致未定义行为。

1.4 const 引用

   有时候我们希望用引用,但不想通过引用修改原变量,这时可以用 const 引用。const 引用可以绑定到常量、临时对象,而普通引用不行。

  • 可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大
  • 常引用可以绑定到临时对象
    临时对象具有常性,表达式运算的结果,调用一个函数,函数传值返回生成的拷贝对象,类型转换等中间都会产生临时对象。临时对象不能被普通引用绑定,但可以被 const 引用绑定。这是 C++ 的一条特殊规则,目的是让函数可以接受临时对象作为参数,避免不必要的拷贝。
  • 所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象。

举个例子:

#include<iostream>
using namespace std;
int main()
{
	const int a = 1;
	//这个写法错误,放大的a的访问权限
	//int& ra = a;
	//正确写法
	const int& ra = a;
	//错误!不能给常量赋值
	//ra++;

	int b = 2;
	//这里的引用是对b访问权限的缩小
	const int& rb = b;
	//错误!不能给常量赋值
	//rb++;
	return 0;
}

再看一个例子:

#include<iostream>
using namespace std;
int main()
{
	int a = 1;
	const int& ra = 30;//ra 不能被改变
	//错误:a*3 是一个临时右值,计算完成后就会销毁,没有持久的内存地址。
	//int& rb = a * 3;
	//正确写法
	const int& rb = a * 3;

	double d = 1.11;
	//错误写法
	//int& rd = d;
	//正确写法
	//编译器需要把d从double转换成int这个转换会生成一个临时的int 变量
	const int& rd = d;
	return 0;
}

2. 指针和引用的关系

  • 语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间
  • 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的
  • 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以不断地改变指向对象
  • 引用可以直接访问指向对象,指针需要解引用才是访问指向对象
  • sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
  • 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些

3. inline修饰函数

  • inline 是一个关键字,用于修饰函数。在函数声明或定义前加上 inline,就表明这个函数是内联函数。编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就需要建立栈帧了,就可以提高效率。
  • inline 只是对编译器的一个建议,编译器有权忽略它。现代编译器非常智能,它们会根据函数复杂度、优化级别等决定是否真的内联。即使你不写 inline,编译器也可能会自动将简单函数内联。反之,即使你写了 inline,如果函数体过大(比如几百行)或包含递归、循环等复杂结构,编译器也可能放弃内联。
  • C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错,inline修饰函数可以,像宏一样展开,但又比宏安全得多。
  • inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。

举个例子:

#include<iostream>
using namespace std;

inline int Add(int x, int y)
{
	int ret = x + y;
	ret += 1;

	return ret;
}
int main()
{
	//可以通过汇编观察程序是否展开
	//有call Add语句就是没有展开,没有就是展开了
	int ret = Add(1, 2);
	cout << Add(1, 2) * 5 << endl;
	return 0;
}

敲黑板inline 是 C++ 提供的一个性能优化工具,但它不是银弹!在编写代码时,优先考虑可读性和正确性,然后才是优化。当遇到性能瓶颈时,可以用 inline 配合分析工具来尝试提升效率。


4. nullptr

   在 C++ 编程中,我们经常需要表示“空指针”——一个不指向任何有效对象的指针。早期 C++ 沿用了 C 语言的 NULL,但在很多场景下,NULL 的表现让人困惑,甚至引发难以察觉的 bug。C++11 引入的nullptr 就是为了彻底解决这些问题。
   我们通常用 NULL 表示空指针。NULL 通常被定义为 0(或 ((void*)0)),这就会带来一些问题!

1. NULL 本质是整数 0
因为 NULL 就是 0,所以它既可以当作整数 0 使用,也可以当作指针使用。这种双重身份在函数重载时会引起混乱。
2. 类型不匹配
在模板推导或自动类型推导中,NULL 被推导为整数类型,而不是指针类型,导致代码行为异常。
3. 无法区分空指针和 0
某些情况下,我们希望重载区分“空指针”和“整数 0”,但 NULL 就是 0,无法区分。
为了解决这些问题,C++11 引入了 nullptr

   nullptr 是一个关键字,表示空指针常量。它有自己的类型 std::nullptr_t(定义在 <cstddef> 中),可以隐式转换为任何指针类型,但不能转换为整数类型(除非用强制转换)。
举个例子:

#include <iostream>
using namespace std;

void f(int x)
{
	cout << "f(int x)" << endl;
}
void f(int* ptr)
{
	cout << "f(int* x)" << endl;
}
int main()
{
	f(0);
	f(NULL);//调用的是整形,不是指针!
	f((int*)NULL);//这样写调用的才是指针

	//void*是一种“通用指针”,但在C++函数重载中,
	// 它不能自动匹配到int*或int 
	//f((void*)NULL);//编译报错
	f(nullptr);
	return 0;
}

效果:
在这里插入图片描述


结语

   C++ 就是这样一门语言——学的时候头大,用的时候真香。
你已经迈出了第一步。接下来,类、继承、模板、STL……每一个新知识都会让你觉得“原来还能这样写!”

代码无bug,学习不迷路,我们下篇再见!(•̀ᴗ•́)و

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐