C-语言特性相关

左值和右值

左值和右值是C/C++编程语言中的两个重要概念,它们在赋值、引用以及类型转换等方面表现出明显的区别。

定义:
  • 左值:表示存储在计算机内存中的对象,具有持久性和可寻址性。即,左值能够用“取地址&”运算符获得其内存地址,且在表达式结束后依然存在。左值可以出现在赋值语句的左侧,也可以出现在右侧。
  • 右值:与左值相对,右值通常表示临时对象,它们不具有持久性和可寻址性。即,右值不能用“取地址&”运算符获得其内存地址,且在表达式结束后就不再存在。右值只能出现在赋值语句的右侧。
左/右值引用
  • 左值引用:左值引用是对左值的引用,即给左值取别名。左值引用在C++中的内部实现通常是一个常量指针,左值引用可以引用非常量左值和常量左值,但不能直接引用右值。
  • 右值引用:右值引用是C++11中引入的新特性,用于引用右值。它允许程序员延长临时对象的生命周期,并在需要时移动资源而不是复制资源。用 && 表示,右值引用只能绑定到右值上,不能绑定到左值上(除非通过 std::move 强制转换)
转换
  • 左值到右值的隐式转换:在大多数情况下,左值可以隐式地转化为右值。例如,在赋值操作中,左侧的左值会“退化”为右值,以便与右侧的右值进行匹配。
  • 右值到左值的显式转换:右值到左值的显式转化通常是不允许的,因为右值在表达式结束后就不再存在。然而,通过 std::move 可以将左值强制转化为右值引用,从而允许对其进行移动操作。
std::move
  • 类型转换: 并不实际移动任何数据,而是将其参数转换为右值引用,从而允许利用移动语义(如果可用)进行资源的转移,而不是复制。

  • 函数原型std::move() 函数原型:move 函数是将任意类型的左值转为其类型的右值引用。

指针

在C++中,指针是一种非常基础且强大的特性,它允许你直接访问和操作内存地址。指针存储了变量的内存地址,而不是变量的值本身。

  • 大小:sizeof获得
    std::cout << "Size of pointer: " << sizeof(int*) << std::endl; // 输出指针大小  
  • 用法:基本用法包括声明、初始化、解引用(或取值)、指针运算等
//声明和初始化
int a = 10;  
int* ptr = &a; // ptr 是一个指向 int 类型的指针,它存储了变量 a 的地址

//解引用,使用 * 运算符来访问指针所指向的值:
std::cout << *ptr << std::endl; // 输出 10

//指针运算,指针可以进行算术运算,但仅限于加上或减去整数(表示偏移量),以及指针之间的比较。
int arr[5] = {1, 2, 3, 4, 5};  
int* p = arr; // p 指向数组的第一个元素  
std::cout << *(p + 2) << std::endl; // 输出 3,因为 p+2 指向了数组的第三个元素
  • 注意事项:
  1. 空指针:未初始化的指针是未定义的,可能指向任意内存地址。通常将指针初始化为nullptr(C++11及以后)或NULL(C++11之前,但在C++11中已不推荐使用)来避免野指针问题。
  2. 越界访问:指针运算时要小心,确保不要越界访问内存。
  3. 悬挂指针:如果指针指向的对象被删除或释放,但该指针未被设置为nullptr,则该指针成为悬挂指针。尝试通过悬挂指针访问内存是未定义行为。
  4. 内存泄漏:使用动态内存分配(如new)时,要确保在适当的时候释放内存(使用delete),以避免内存泄漏。
  5. 智能指针:C++11及以后版本中引入了智能指针(如std::unique_ptrstd::shared_ptr等),它们可以自动管理动态分配的内存,减少内存泄漏的风险。
  6. 指针和数组:在C++中,数组名在大多数情况下会被视为指向数组首元素的指针。但是,这种用法有一些陷阱,比如数组名本身不是一个可修改的左值。
指针和引用区别

指针和引用的主要区别可以简单归纳如下:

  1. 定义与性质

    • 指针:是一个变量,存储的是另一个变量的内存地址。
    • 引用:是某个变量的别名,与原变量共享同一块内存空间,实质上是同一个东西。
  2. 内存分配

    • 指针:需要分配内存空间来存储地址值。
    • 引用:不需要分配额外的内存空间,它只是一个别名。
  3. 初始化和赋值

    • 指针:可以在定义时不初始化,之后可以指向任何有效的内存地址,包括NULL。
    • 引用:必须在定义时初始化,且之后不能再改变为引用另一个变量。
  4. 多级性

    • 指针:可以有多级,例如指向指针的指针。
    • 引用:只能是一级的,不能有多级引用。
  5. 空值

    • 指针:可以为空(NULL或nullptr)。
    • 引用:不能为NULL,必须始终引用一个有效的对象。
  6. sizeof运算符

    • 指针:使用sizeof返回的是指针本身的大小(通常是4字节或8字节,取决于系统架构)。
    • 引用:使用sizeof返回的是被引用变量的大小。
  7. 运算操作

    • 指针:支持算术运算(如加减)和比较运算。
    • 引用:不支持算术运算,主要用于别名访问。
  8. 函数参数

    • 指针和引用作为函数参数时,都可以改变实参的值,但引用在语法上更为简洁,且使用上更安全。
  9. 安全性

    • 指针:使用时需要更加小心,因为错误的指针操作可能导致程序崩溃或安全问题。
    • 引用:相对更安全,因为它在定义时必须初始化,并且不能改变为引用另一个变量。
常量指针和指针常量

常量指针和指针常量在C++中是两种常见的指针类型,尽管它们都涉及到const关键字,但它们的含义和应用场景存在明显的区别。以下是对这两种指针的详细比较:

本质区别
  • 常量指针:本质上是一个指针,const修饰的是指针指向的内容,表示该指针指向一个“常量”,即指针指向的变量的值不能通过该指针来改变。
  • 指针常量:本质上是一个常量,const修饰的是指针本身,表示该常量是一个指针类型的常量,即指针的值(即它所指向的地址)不能改变。
声明方式
  • 常量指针:通常声明为const 数据类型 *指针变量名,例如const int *p = &a;,这里p是一个指向整型常量的指针,不能通过p来修改a的值,但p可以指向另一个整型变量的地址。
  • 指针常量:通常声明为数据类型 * const 指针变量名,例如int *const q = &a;,这里q是一个指针常量,它的值(即它指向的地址)不能被修改,但可以通过q来修改它所指向的变量的值(如果那个变量不是常量的话)。
应用场景
  • 常量指针:常用于需要保护数据不被意外修改的场景,同时又想通过指针来访问这些数据。

  • 指针常量:常用于需要确保指针始终指向同一个地址的场景,比如某些资源的句柄或引用,一旦初始化后就不应该再指向其他地址。

  • 常量指针示例

    int a = 10;
    const int *p = &a; // p 是常量指针,指向 a
    // *p = 20; // 编译错误,不能通过 p 修改 a 的值
    p = &b; // 正确,p 可以指向另一个变量 b
    
  • 指针常量示例

    int a = 10;
    int *const q = &a; // q 是指针常量,指向 a
    *q = 20; // 正确,可以通过 q 修改 a 的值
    // q = &b; // 编译错误,q 不能指向其他地址
    
函数指针

函数指针是C和C++语言中的一个特性,它允许程序员将函数的地址存储在变量中,并通过这个变量来调用函数。函数指针的概念是高级且强大的,因为它提供了一种机制来动态地调用不同的函数,基于运行时条件。

函数指针的声明

函数指针的声明需要指定函数返回值的类型、函数名(在这里我们使用指针名代替)以及函数的参数列表(包括参数的类型和数量)。但是,在声明函数指针时,我们不需要函数名,而是使用指针名来引用这个函数。

例如,假设我们有一个函数原型如下:

int add(int a, int b);

要声明一个指向这个函数的指针,我们可以这样写:

int (*ptrToAdd)(int, int);

这里,ptrToAdd是一个指针,它指向一个函数,该函数接受两个int类型的参数并返回一个int类型的值。

函数指针的赋值

一旦我们声明了函数指针,就可以将函数的地址赋给它。在C和C++中,函数名就代表了函数的地址,因此我们可以直接将函数名赋给函数指针。

ptrToAdd = add; // 将add函数的地址赋给ptrToAdd
通过函数指针调用函数

有了函数指针之后,我们就可以通过这个函数指针来调用函数了。调用方式是通过解引用函数指针,并像调用普通函数一样传递参数。

int result = ptrToAdd(5, 3); // 等同于调用 add(5, 3)
函数指针的应用

函数指针在C和C++中有很多应用,包括但不限于:

  • 回调函数:在某些API中,你可以将函数指针作为参数传递给另一个函数,这个被传递的函数指针指向的函数会在某个特定事件发生时被调用。
  • 排序函数:例如,在C标准库中的qsort函数,它接受一个比较函数的指针作为参数,这个比较函数用于定义排序的准则。
  • 动态函数表:通过函数指针数组,可以实现基于运行时条件的函数选择,这在实现多态或构建插件系统时非常有用。
  • 事件处理:在图形用户界面(GUI)编程中,事件处理函数通常是通过函数指针来指定的,这样当特定事件(如按钮点击)发生时,相应的函数就会被调用。
值/引用/指针传递

在参数传递中,值传递、引用传递和指针传递是C++(以及C语言)中常见的三种方式,它们各自具有不同的特点和用途。以下是这三种传递方式的详细区别:

值传递(Pass by Value)

定义:值传递时,形参是实参的副本(复制、拷贝)。即函数接收的是实参的一个拷贝,函数体内对形参的任何修改都不会影响到实参。

特点

  • 单向性:数据只能从实参传递到形参,不能反向传递。
  • 独立性:形参和实参是两个独立的变量,它们占据不同的内存空间。
  • 效率:对于大型对象或结构体,值传递可能会导致较高的内存开销和性能损耗,因为需要复制整个对象。

示例

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    // 这里a和b的交换不会影响实参
}
引用传递(Pass by Reference)

定义:引用传递时,实参的引用(即内存地址)被传递给形参。形参和实参指向同一块内存地址,因此函数体内对形参的修改会直接影响到实参。

特点

  • 双向性:数据可以在实参和形参之间双向传递。
  • 共享性:形参和实参共享同一块内存空间,对形参的修改会反映到实参上。
  • 效率:对于大型对象或结构体,引用传递可以避免不必要的复制,提高效率。

示例

void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
    // 这里a和b的交换会影响实参
}
指针传递(Pass by Pointer)

定义:指针传递时,实参的地址(即指针)被传递给形参。形参是一个指针变量,它存储了实参的地址,因此函数体内可以通过解引用形参来修改实参的值。

特点

  • 间接访问:通过指针可以间接访问和修改实参的值。
  • 灵活性:指针传递提供了更高的灵活性,可以动态地修改多个变量的值。
  • 风险:指针操作相对复杂,容易出错,如野指针、空指针解引用等问题。

示例

void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
    // 这里通过解引用指针来交换实参的值
}
适用
  • 值传递:适用于不需要修改实参的场景,或者实参是基本数据类型时。
  • 引用传递:适用于需要修改实参的场景,特别是当实参是大型对象或结构体时,可以提高效率。
  • 指针传递:提供了更高的灵活性,但也需要更谨慎的操作,以避免指针相关的错误。
迭代器

迭代器模式(Iterator Pattern)是一种行为型设计模式,它提供了一种方法顺序访问一个容器对象中的各个元素,而又不暴露其内部的细节。

迭代器模式的核心思想是将集合的遍历功能从集合对象中分离出来,形成一个独立的迭代器对象来管理访问集合元素的逻辑。

  • 迭代器为不同数据结构提供统一的访问接口,使得遍历数据变得简单统一,同时支持高效处理大型数据集,通过逐个访问元素节省内存资源。

  • 迭代器不仅限于遍历,还能与算法结合实现复杂数据处理,支持惰性计算,提升代码可读性和可维护性,是现代编程中不可或缺的工具。

野指针和悬空指针

野指针和悬空指针是编程中常见的两种指针问题,它们通常会导致程序出现不可预测的行为和潜在的错误。

野指针(Wild Pointer)

定义
野指针是指向一个已删除的对象或未申请访问受限内存区域的指针。野指针的值是不确定的,可能指向任何位置,包括操作系统不允许访问的内存区域。

危害
访问野指针通常会导致程序崩溃或未定义行为,因为它可能指向任意内存地址,包括操作系统不允许访问的内存区域。

成因

  • 指针变量未初始化:任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。如果指针变量在创建时没有初始化,它将指向一个不确定的内存地址,从而成为野指针。
  • 指针释放之后未置空:有时指针在free或delete后未赋值NULL,此时指针仍然指向已被释放的内存地址,但该内存可能已经被系统重新分配给其他对象,因此该指针成为野指针。
  • 指针操作超越变量作用域:例如,函数返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放,返回的指针将成为野指针。

预防措施

  • 初始化时置NULL:指针变量在创建时应立即初始化为NULL或有效的内存地址。
  • 释放时置NULL:当指针指向的内存空间释放时,应立即将指针置为NULL,防止产生野指针。
  • 避免返回局部变量的地址:不要从函数中返回局部变量的地址,因为局部变量在函数返回后会被销毁。
悬空指针(Dangling Pointer)

定义
悬空指针是指一个曾经指向有效内存区域,但由于该内存区域已经被释放或变得无效,因此现在指向一个不再可用的内存地址的指针。

危害
尝试通过悬空指针访问内存通常会导致程序崩溃或未定义行为,因为指针指向的内存区域已经不再可用。

成因

  1. 内存释放后继续使用:动态分配的内存被释放(如使用free()函数),但指针仍然指向原来的内存地址。
  2. 函数返回局部变量的地址:与野指针类似,但悬空指针特指在内存被释放后仍然指向该内存地址的情况。
  3. 使用已经释放的资源:例如,文件被关闭后,仍然使用指向该文件的指针。

预防措施

  • 释放内存后置空指针:在使用free()或delete释放动态分配的内存后,立即将指针置为NULL。
  • 避免返回局部变量的地址:同上,不要从函数中返回局部变量的地址。
  • 谨慎使用动态内存分配:尽量减少动态内存分配的使用,特别是在不需要的情况下。如果必须使用,确保正确管理内存的生命周期。
nullptr 与 NULL
  • NULL:预处理变量,是一个宏,它的值是 0,定义在头文件 中,即 #define NULL 0。
  • nullptr:C++ 11 中的关键字,是一种特殊类型的字面值,可以被转换成任意其他类型。
nullptr优势:
  • 有类型,类型是 typdef decltype(nullptr) nullptr_t;,使用 nullptr 提高代码的健壮性。
  • 函数重载:因为 NULL 本质上是 0,在函数调用过程中,若出现函数重载并且传递的实参是 NULL,可能会出现不知和哪一个函数匹配的情况;但是传递实参 nullptr 就不会出现这种情况。
上一篇:pip常用命令详解


下一篇:Java-List集合堆内存溢出-情况一