【C++篇】启航——初识C++(下篇)

2024-10-09 15:09:53 浏览数 (2)

接上篇【C 篇】启航——初识C (上篇)

一、引用

1.引用的概念

引用(Reference)是 C 中的一种类型,它提供了一个变量的别名。引用并不是一种独立的数据类型,而是对已有变量的另一种视图。引用的声明使用 & 符号。

引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。⽐如:⽔壶传中李逵,宋江叫"铁⽜",江湖上⼈称"⿊旋⻛";林冲,外号豹⼦头;

2.引用的基本语法

代码语言:javascript复制
int a = 10;       // 定义一个整数变量
int &b = a;      // b 是 a 的引用

在上面的例子中,b 作为 a 的引用,ba 是同一个对象,修改 b 的值实际上会改变 a 的值。

3.引用的特点

引用的特点: 1.别名:引用是一个变量的别名,对引用的所有操作实际上都是对原变量的操作。 2.不占用额外内存:引用不占用额外的内存空间,只是另一个指向相同内存地址的标识符。 3.必须初始化:引用在创建时必须初始化,并且一旦初始化后不可改变绑定的对象。 4.不能为 NULL:引用不能被赋值为 nullptr,必须引用一个有效的对象。

3.1 别名

引用是一个变量的别名。这意味着对引用的所有操作都是直接对其所引用的变量的操作。引用没有独立的内存空间,它只是在原变量的基础上提供了一个新的名字。

代码语言:javascript复制
#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;
}

在这个示例中,ba 的引用,对 b 的修改直接影响 a,反之亦然。

3.2 不占用额外内存

引用本质上是一个别名,不会占用新的内存空间。它只是指向已有变量的地址。因此,引用操作不会增加内存的使用。

代码语言:javascript复制
#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 必须初始化

引用在创建时必须被初始化。它不能在声明后再被赋值或指向其他变量。这一特性使得引用在使用时更加安全,避免了指向无效对象的风险。

代码语言:javascript复制
#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,它必须引用一个有效的对象。这意味着引用在创建后始终是有效的,避免了指向空地址的风险。

代码语言:javascript复制
#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 函数参数传递

使用引用作为函数参数可以有效避免大对象的复制,从而节省内存和时间。通过引用传递参数,函数可以直接修改原始数据,而无需创建副本。

代码语言:javascript复制
#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 中的函数可以返回引用,这样可以在函数外部直接修改原始数据。这种方式在某些情况下可以提高效率,但需要谨慎使用,尤其是返回局部变量的引用是危险的。

代码语言:javascript复制
#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 引用)允许我们通过引用访问变量,但不允许修改它。这在需要保护数据不被意外修改时非常有用,尤其是在传递大型对象时,可以避免复制并保护原始数据。

代码语言:javascript复制
#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).初始化

引用:在定义引用时,必须立即初始化并引用一个有效的对象。一旦绑定到某个变量后,就无法改变引用的对象。

代码语言:javascript复制
int a = 10;
int &b = a; // 必须初始化

指针:指针在定义时不需要初始化,可以稍后赋值。指针可以随时指向不同的对象。q

代码语言:javascript复制
int *p; // 不初始化,指向未知
int a = 10;
p = &a; // 指向 a
(3).改变指向

引用:引用一旦初始化后,就不可以再改变引用的对象。

代码语言:javascript复制
int a = 10;
int &b = a;
// b = 20; // 这将改变 a 的值为 20,但 b 仍然引用 a

指针:指针可以在程序运行时动态改变指向的对象。

代码语言:javascript复制
int a = 10;
int b = 20;
int *p = &a; // p 指向 a
p = &b;      // p 现在指向 b
(4).访问对象

引用:可以直接使用引用访问所引用的对象,语法上更简洁。

代码语言:javascript复制
int a = 10;
int &b = a;
std::cout << b; // 直接访问

指针:需要使用解引用操作符 * 访问指针指向的对象。

代码语言:javascript复制
int a = 10;
int *p = &a;
std::cout << *p; // 解引用访问
(5).内存大小

引用:在 sizeof 运算中,引用的结果是引用对象的大小,不占用额外的内存。

代码语言:javascript复制
int a = 10;
int &b = a;
std::cout << sizeof(b); // 输出 sizeof(int)

指针:在 sizeof 运算中,指针的大小是固定的(在 32 位平台上通常为 4 字节,64 位平台上为 8 字节)。

代码语言:javascript复制
int *p;
std::cout << sizeof(p); // 输出 4 或 8
(6).安全性

引用:因为引用不能为 NULL,也不会出现悬挂引用的问题,所以相对更安全。

指针:指针容易出现空指针和悬挂指针的问题,需要额外的小心和处理。

代码语言:javascript复制
int *p = nullptr; // 空指针
// int a = *p; // 会导致未定义行为

二、inline

1.定义

inline是C 中的一个关键字,主要用于建议编译器在调用函数的地方直接插入该函数的代码,而不是通过常规的函数调用。这通常用于小型函数,以减少函数调用的开销。

2.使用方法

在C 中,使用inline非常简单。你只需在函数定义前加上inline关键字。例如:

代码语言:javascript复制
inline int add(int a, int b) {
    return a   b;
}

3.优点

  1. 性能提升:通过减少函数调用的开销(如压栈、弹栈等),可以提高程序性能,尤其是在频繁调用的小函数中。
  2. 代码可读性:小函数的使用使得代码更加模块化和易于理解。

4.注意事项

  1. 编译器的决定:虽然你可以建议编译器使用inline,但编译器并不一定会接受这个建议。它可能根据函数的复杂度和其他因素决定是否进行内联。
  2. 代码膨胀:如果一个inline函数被多次调用,编译器会在每个调用点插入函数体,可能导致代码膨胀,增加最终二进制文件的大小。
  3. 调试困难:内联函数在调试时可能会使得调用栈不如预期,因为调用点会被替换为函数体。

5.适用场景

  • 短小函数:适合将那些逻辑简单、体积小的函数标记为inline
  • 频繁调用的函数:例如,在循环中频繁调用的简单函数,使用inline可能会有显著性能提升。

三、nullptr

NULL实际是⼀个宏,在传统的C头⽂件( stddef.h )中,可以看到如下代码:

代码语言:javascript复制
#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 只能被

隐式地转换为指针类型,⽽不能被转换为整数类型。

代码语言:javascript复制
#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 中的重要特性,它们在代码的可读性、性能和安全性上都有显著影响。了解并合理使用这些特性,有助于编写出高效且可维护的代码。希望这篇博客对你有所帮助!如果有任何问题或想法,欢迎在评论区交流!

0 人点赞