一、引用
1.引用的概念
引用(Reference)是 C++ 中的一种类型,它提供了一个变量的别名。引用并不是一种独立的数据类型,而是对已有变量的另一种视图。引用的声明使用 &
符号。
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。⽐如:⽔壶传中李逵,宋江叫"铁⽜",江湖上⼈称"⿊旋⻛";林冲,外号豹⼦头;
2.引用的基本语法
int a = 10; // 定义一个整数变量 int &b = a; // b 是 a 的引用
在上面的例子中,b
作为 a
的引用,b
和 a
是同一个对象,修改 b
的值实际上会改变 a
的值。
3.引用的特点
引用的特点:
1.别名:引用是一个变量的别名,对引用的所有操作实际上都是对原变量的操作。
2.不占用额外内存:引用不占用额外的内存空间,只是另一个指向相同内存地址的标识符。
3.必须初始化:引用在创建时必须初始化,并且一旦初始化后不可改变绑定的对象。
4.不能为 NULL:引用不能被赋值为
nullptr
,必须引用一个有效的对象。
3.1 别名
引用是一个变量的别名。这意味着对引用的所有操作都是直接对其所引用的变量的操作。引用没有独立的内存空间,它只是在原变量的基础上提供了一个新的名字。
#include <iostream> int main() { int a = 42; // 定义一个整数变量 a int &b = a; // b 是 a 的引用 std::cout << "a: " << a << ", b: " << b << std::endl; // 输出 a: 42, b: 42 b = 100; // 通过引用 b 修改 a 的值 std::cout << "After changing b..." << std::endl; std::cout << "a: " << a << ", b: " << b << std::endl; // 输出 a: 100, b: 100 return 0; }
在这个示例中,b
是 a
的引用,对 b
的修改直接影响 a
,反之亦然。
3.2 不占用额外内存
引用本质上是一个别名,不会占用新的内存空间。它只是指向已有变量的地址。因此,引用操作不会增加内存的使用。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; int main() { int a = 0; // 引⽤:b和c是a的别名 int& b = a; int& c = a; // 也可以给别名b取别名,d相当于还是a的别名 int& d = b; ++d; // 这⾥取地址我们看到是⼀样的 cout <<"a:" << &a << endl; cout <<"b:" << &b << endl; cout <<"c:" << &c << endl; cout <<"d:" << &d << endl; return 0; }
在这个例子中,a、b、c、d的地址相同,证明引用并不占用额外的内存空间。
3.3 必须初始化
引用在创建时必须被初始化。它不能在声明后再被赋值或指向其他变量。这一特性使得引用在使用时更加安全,避免了指向无效对象的风险。
#include <iostream> int main() { int a = 5; // int &b; // 错误:引用必须在声明时初始化 int &b = a; // 正确:b 在声明时被初始化为 a std::cout << "a: " << a << ", b: " << b << std::endl; // 输出 a: 5, b: 5 return 0; }
试图声明一个未初始化的引用 b
会导致编译错误,而在初始化时,引用可以安全地与一个变量绑定。
3.4 不能为 NULL
引用不能被赋值为 nullptr
,它必须引用一个有效的对象。这意味着引用在创建后始终是有效的,避免了指向空地址的风险。
#include <iostream> int main() { int a = 10; int &b = a; // 正确,b 引用 a // int &c = nullptr; // 错误:引用不能为 NULL std::cout << "b: " << b << std::endl; // 输出 b: 10 return 0; }
试图将引用 c
赋值为 nullptr
会导致编译错误。这确保了引用始终指向有效的对象。
4.引用的使用
4.1 函数参数传递
使用引用作为函数参数可以有效避免大对象的复制,从而节省内存和时间。通过引用传递参数,函数可以直接修改原始数据,而无需创建副本。
#include <iostream> void increment(int &num) { num += 1; // 直接修改原始数据 } int main() { int value = 5; increment(value); // 传递 value 的引用 std::cout << "Incremented value: " << value << std::endl; // 输出 6 return 0; }
在这个示例中,increment
函数接受 num
的引用,对 num
的修改直接影响 value
,避免了复制的开销。
4.2 返回值
C++ 中的函数可以返回引用,这样可以在函数外部直接修改原始数据。这种方式在某些情况下可以提高效率,但需要谨慎使用,尤其是返回局部变量的引用是危险的。
#include <iostream> int& getReference(int &x) { return x; // 返回 x 的引用 } int main() { int a = 10; getReference(a) = 20; // 直接修改 a std::cout << "Updated value: " << a << std::endl; // 输出 20 return 0; }
4.3 常量引用
常量引用(const
引用)允许我们通过引用访问变量,但不允许修改它。这在需要保护数据不被意外修改时非常有用,尤其是在传递大型对象时,可以避免复制并保护原始数据。
#include <iostream> void printValue(const int &num) { std::cout << "Value: " << num << std::endl; // 只读操作 } int main() { int a = 10; printValue(a); // 输出 10 printValue(20); // 可以传递字面量,输出 20 return 0; }
在这个例子中,printValue
函数接受 const int &num
作为参数,意味着它只能读取 num
的值,而不能修改。这样不仅保证了数据的安全性,还避免了复制的开销。
5.引用和指针的关系
引用和指针是 C++ 中两个重要的概念,它们都可以用于间接访问变量,但在语法、功能和使用方式上存在显著差异。下面将从几个方面比较它们。
(1).基本定义
引用:引用是一个变量的别名,它指向一个已有变量,并且在创建时必须初始化。引用不占用额外的内存空间,只是原变量的另一个名称。
指针:指针是一个变量,它存储一个地址,指向另一个变量的内存位置。指针在定义时不一定要初始化,可以在之后赋值。
(2).初始化
引用:在定义引用时,必须立即初始化并引用一个有效的对象。一旦绑定到某个变量后,就无法改变引用的对象。
int a = 10; int &b = a; // 必须初始化
指针:指针在定义时不需要初始化,可以稍后赋值。指针可以随时指向不同的对象。q
1. int *p; // 不初始化,指向未知 2. int a = 10; 3. p = &a; // 指向 a
(3).改变指向
引用:引用一旦初始化后,就不可以再改变引用的对象。
int *p; // 不初始化,指向未知 int a = 10; p = &a; // 指向 a
指针:指针可以在程序运行时动态改变指向的对象。
int a = 10; int &b = a; // b = 20; // 这将改变 a 的值为 20,但 b 仍然引用 a
(4).访问对象
引用:可以直接使用引用访问所引用的对象,语法上更简洁。
int a = 10; int &b = a; std::cout << b; // 直接访问
指针:需要使用解引用操作符 *
访问指针指向的对象。
int a = 10; int *p = &a; std::cout << *p; // 解引用访问
(5).内存大小
引用:在 sizeof
运算中,引用的结果是引用对象的大小,不占用额外的内存。
int a = 10; int &b = a; std::cout << sizeof(b); // 输出 sizeof(int)
指针:在 sizeof
运算中,指针的大小是固定的(在 32 位平台上通常为 4 字节,64 位平台上为 8 字节)。
int *p; std::cout << sizeof(p); // 输出 4 或 8
(6).安全性
引用:因为引用不能为 NULL
,也不会出现悬挂引用的问题,所以相对更安全。
指针:指针容易出现空指针和悬挂指针的问题,需要额外的小心和处理。
int *p = nullptr; // 空指针 // int a = *p; // 会导致未定义行为
二、inline
1.定义
inline
是C++中的一个关键字,主要用于建议编译器在调用函数的地方直接插入该函数的代码,而不是通过常规的函数调用。这通常用于小型函数,以减少函数调用的开销。
2.使用方法
在C++中,使用inline
非常简单。你只需在函数定义前加上inline
关键字。例如:
inline int add(int a, int b) { return a + b; }
3.优点
- 性能提升:通过减少函数调用的开销(如压栈、弹栈等),可以提高程序性能,尤其是在频繁调用的小函数中。
- 代码可读性:小函数的使用使得代码更加模块化和易于理解。
4.注意事项
- 编译器的决定:虽然你可以建议编译器使用
inline
,但编译器并不一定会接受这个建议。它可能根据函数的复杂度和其他因素决定是否进行内联。 - 代码膨胀:如果一个
inline
函数被多次调用,编译器会在每个调用点插入函数体,可能导致代码膨胀,增加最终二进制文件的大小。 - 调试困难:内联函数在调试时可能会使得调用栈不如预期,因为调用点会被替换为函数体。
5.适用场景
- 短小函数:适合将那些逻辑简单、体积小的函数标记为
inline
。 - 频繁调用的函数:例如,在循环中频繁调用的简单函数,使用
inline
可能会有显著性能提升。
三、nullptr
NULL实际是⼀个宏,在传统的C头⽂件( stddef.h )中,可以看到如下代码:
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
• C++中 NULL 可能被定义为字⾯常量0,或者C中被定义为⽆类型指针( void* )的常量。不论采取何种定义,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦,本想通过 f(NULL) 调⽤指针版本的
f(int*) 函数,但是由于 NULL 被定义成0,调⽤了 f(int x) ,因此与程序的初衷相悖。 f((void*)NULL) ;
调⽤会报错。
• C++11中引⼊ nullptr , nullptr 是⼀个特殊的关键字, nullptr 是⼀种特殊类型的字⾯量,它可以转换
成任意其他类型的指针类型。使⽤ nullptr 定义空指针可以避免类型转换的问题,因为 nullptr 只能被
隐式地转换为指针类型,⽽不能被转换为整数类型。
#include<iostream> using namespace std; 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(int*)函数 //但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。 f(NULL); f((int*)NULL); // 编译报错:“f”: 2 个重载中没有⼀个可以转换所有参数类型 // f((void*)NULL); f(nullptr); return 0; }
总结
引用、内联函数和 nullptr
是 C++ 中的重要特性,它们在代码的可读性、性能和安全性上都有显著影响。了解并合理使用这些特性,有助于编写出高效且可维护的代码。希望这篇博客对你有所帮助!如果有任何问题或想法,欢迎在评论区交流!