C/C++指针&智能指针一

C/C++指针&智能指针

1.指针的基本概念

C/C++中的指针是一个核心概念,它极大地增强了语言的灵活性和效率。

1. 变量的地址

在C/C++中,每个在内存中分配了空间的变量都有一个唯一的地址,这个地址标识了该变量在内存中的位置。你可以通过取地址运算符&来获取一个变量的地址。例如:

int number = 42;
int *ptr = &number; // ptr 存储了变量 number 的地址

2. 指针变量

指针变量是一种特殊的变量,它存储的是另一个变量的起始地址而不是数据本身。声明指针变量时,需要指定指针所指向的数据类型。例如,声明一个指向整型变量的指针,语法如下:

int *ptr; // 声明一个指针变量,它可以存储int类型变量的地址

3. 对指针赋值

对指针赋值,实际上就是让指针指向某个变量的地址。这可以通过直接赋予变量地址(使用取地址运算符&)或另一个相同类型的指针变量来完成。

1.直接赋值变量地址
int value = 100;
int *ptr = &value; // ptr 现在指向了 value 的地址
2.指针间赋值
int anotherValue = 200;
int *anotherPtr = &anotherValue;
ptr = anotherPtr; // 现在 ptr 指向了 anotherValue 的地址

还可以将空值(如 NULLnullptr)赋给指针,表示它不指向任何有效的内存位置:

ptr = nullptr; // C++ 中推荐使用 nullptr
// 或在C语言中
ptr = NULL; // 表示指针不再指向任何有效的内存

正确管理和使用指针是C/C++编程中的关键技能,但同时也需要谨慎处理,以避免诸如内存泄漏、野指针或悬挂指针等问题。

2.使用指针

在使用指针时,正确地对指针变量进行赋值是至关重要的,以确保程序的稳定性和安全性。

1.初始化指针

  • 初始化为NULL或nullptr:最好在声明指针时就将其初始化为NULL(C语言)或nullptr(C++)。这样可以防止指针包含一个不确定的值,减少出现野指针的风险。

    int *ptr = nullptr; // C++
    // 或在C中
    int *ptr = NULL;
    
  • 指向已分配的内存:如果指针用于指向动态分配的内存,使用如new(C++)或malloc(C语言)分配内存后,应立即将返回的地址赋给指针。

    int *ptr = new int; // C++
    // 或在C中
    int *ptr = (int*)malloc(sizeof(int));
    

2.指针的解引用

指针的解引用是指通过指针访问其指向的内存位置上的数据。在C/C++中,使用*运算符来实现指针的解引用。当对一个指针进行解引用时,编译器会查看该指针存储的内存地址,并返回那个地址上数据的内容。

假设有一个指向整型变量的指针int *ptr,并且它已经被正确初始化指向一个整数:

int num = 42;
int *ptr = # // ptr 指向 num 的地址

要访问并可能修改num的值,你可以解引用ptr

// 访问值
int value = *ptr; // value 现在等于 42

// 修改值
*ptr = 100; // num 的值现在变为 100

对于指向指针的指针(如int **ptr),你需要使用多个*来逐层解引用:

int num = 42;
int *ptr1 = #
int **ptr2 = &ptr1;

// 解引用 ptr2 得到 ptr1,再解引用 ptr1 得到 num 的值
int value = **ptr2; // value 等于 42

在指针上执行算术后解引用,可以访问数组或连续内存区域中的元素:

int array[5] = {1, 2, 3, 4, 5};
int *ptr = array; // ptr 指向数组的首元素

// 访问数组的第二个元素
int secondElement = *(ptr + 1); // secondElement 等于 2
  • 在解引用指针前,确保它不是NULLnullptr,否则会导致未定义行为,很可能会引起程序崩溃。
  • 确保指针指向的内存是可访问的,没有越界,特别是在使用指针算术时。

正确使用解引用操作符是操作指针数据的基础,它使得指针成为C/C++中一种强大而灵活的工具。

3.指针拷贝与交换

  • 指针之间可以直接赋值,这实际上是拷贝了地址,而不是指向的数据。确保当这样做时,原始指针和副本指向的内存是有效且一致使用的。

    int *ptr1 = new int(10);
    int *ptr2 = ptr1; // ptr2 现在也指向同一个 int 实例
    
  • 避免悬挂指针,即不要在释放了某块内存后,仍然有指针指向这块已经被释放的内存。

3.指针用于函数的参数

指针作为函数参数在C/C++中是非常实用的特性,它允许函数直接访问和修改外部变量的值,同时还能高效地处理大型数据结构。

1. 传递大型数据或对象

当函数需要处理大型数据结构(如数组、结构体或类对象)时,直接传递它们的副本会非常低效,尤其是当这些数据很大时。通过传递指向这些数据的指针或引用(C++中),可以避免复制整个数据,仅需传递数据的地址即可。

void printArray(int *array, int size) {
    for(int i = 0; i < size; ++i) {
        printf("%d ", array[i]);
    }
}

// 调用示例
int arr[] = {1, 2, 3, 4, 5};
printArray(arr, sizeof(arr)/sizeof(arr[0]));

2. 修改外部变量

普通变量作为参数传递给函数时,函数内部对参数的修改不会影响到外部变量的值,因为参数是按值传递的。但如果传递的是变量的地址(指针),函数就可以通过指针修改外部变量的值。

void increment(int *num) {
    (*num)++;
}

// 调用示例
int value = 10;
increment(&value);
printf("Value after increment: %d\n", value); // 输出 11

3. 动态内存分配

指针作为函数参数时,还可以用于动态地在函数内部分配内存,然后将分配的内存地址返回给调用者,这在某些场景下非常有用。

int *allocateMemory(int size) {
    return (int*)malloc(size * sizeof(int));
}

// 调用示例
int *myArray = allocateMemory(5);

4.指针用于函数的返回值

在C/C++中,指针也可以作为函数的返回值,这在很多情况下都非常有用,特别是当函数需要返回复杂数据结构或者需要修改外部数据时。

  1. 返回动态分配的内存:函数可以创建并返回指向新分配内存的指针,这样调用者就可以访问和管理这块内存。

    char* createString(const char* text) {
        char* result = (char*)malloc(strlen(text) + 1);
        strcpy(result, text);
        return result;
    }
    
  2. 访问静态或全局数据:有时函数需要返回对静态或全局数据的访问权限,这时返回指向这些数据的指针是合适的。

    extern int globalData;
    int* getGlobalDataPointer() {
        return &globalData;
    }
    
  3. 遍历数据结构:对于链表、树等复杂数据结构,函数可以返回指向下一个节点的指针,以便进行遍历。

    struct Node {
        int data;
        Node* next;
    };
    
    Node* getNextNode(Node* currentNode) {
        return currentNode->next;
    }
    

5.用const修饰指针

在C/C++中,使用const关键字修饰指针可以用来限制指针的用途,确保数据的安全性和代码的健壮性。const修饰指针有几种不同的形式,每种形式有不同的含义。

1. 指向常量的指针 (const位于星号左侧)

const int *ptr;

这种情况下,ptr是一个指向常量整型数据的指针。你可以通过指针读取数据,但不能通过指针修改数据。这意味着*ptr(解引用后得到的值)的值是不可变的。

2. 指针自身为常量 (const位于星号右侧)

int *const ptr = &someValue;

这里,ptr是一个常量指针,指向一个整型数据。一旦ptr被初始化指向某个地址,它就不能再指向其他地址,但可以通过指针修改所指向的数据(可以修改解引用后得到的值)。

3. 指向常量的常量指针 (const位于星号两侧)

const int *const ptr = &someValue;

这是上述两种情况的结合,意味着ptr既不能指向其他地址(初始化后的存放的地址不可更改),也不能通过指针修改所指向的数据的值。

使用场景

  • 保护数据:当你希望确保数据不被意外修改时,可以使用指向常量的指针。
  • 接口设计:在函数参数中使用const指针可以让调用者知道该函数不会修改传入的数据,增加接口的透明度和安全性。
  • 实现常量成员函数:在C++类中,成员函数如果声明为const,那么它内部不能修改类的任何成员变量。当这样的函数需要访问类的成员变量时,会使用指向常量的成员指针或引用。

6.void关键字在指针中的应用

void关键字在C/C++的指针中扮演了一个特殊而重要的角色。

1. 无类型指针 (void *)

void *是一种通用指针类型,表示可以指向任何类型的数据。这种类型的指针主要用于:

  • 通用数据处理:在不知道具体类型或者需要编写通用代码时,可以使用void *。例如,在内存操作、数据传输或某些回调函数中。

    void *data = malloc(sizeof(int)); // 动态分配内存
    free(data); // 释放内存时不需要知道具体类型
    
  • 函数参数:当函数不关心或不需指定参数的具体类型时,可以使用void *。这常见于需要处理多种类型数据的通用函数或回调函数。

    void printValue(void *value) {
        // 在此例中,我们假设value指向的是一个整数,仅为演示
        int *intValue = (int *)value;
        printf("%d\n", *intValue);
    }
    
  • 使用void *指针时,必须小心进行类型转换,因为编译器不会自动检查类型安全。

  • 在转换void *到具体类型指针时,确保转换是合法且符合预期的,避免类型不匹配导致的问题。

  • 虽然void *提供了极大的灵活性,过度使用可能导致代码难以理解和维护,尤其是在类型安全至关重要的场合。

7.动态分配内存new/delete

在C++中,newdelete是用于动态内存分配和释放的一对操作符。动态内存分配允许程序在运行时根据需要请求内存,这对于不确定数据大小或需要在程序运行过程中改变数据结构的情况非常有用。下面是newdelete的基本使用方法和注意事项:

1.动态分配单个对象

int *ptr = new int; // 分配一个整型大小的内存空间,并返回指向该空间的指针
*ptr = 42; // 通过指针给分配的内存赋值
delete ptr; // 使用完毕后,释放该内存

2.动态分配数组

int *arr = new int[10]; // 分配一个包含10个整数的数组
arr[0] = 1; // 访问和修改数组元素
delete[] arr; // 释放数组所占的内存,注意使用`[]`

3.动态分配对象

对于类对象,new还可以调用构造函数来初始化对象。

class MyClass {
public:
    MyClass(int x) : value(x) {} // 构造函数
private:
    int value;
};

MyClass *obj = new MyClass(42); // 分配并构造一个MyClass对象
delete obj; // 释放对象占用的内存,会调用析构函数

8.二级指针

二级指针是C/C++中的一种高级指针用法,它是指向指针的指针,即一个指针变量的地址被另一个指针变量所存储。

1. 动态二维数组

二级指针可以用来动态创建和管理二维数组,其中一级指针指向行,二级指针则指向这些行指针的集合。

int rows = 3, cols = 4;
int **matrix = new int *[rows];
for(int i = 0; i < rows; ++i) {
    matrix[i] = new int[cols];
}
// 使用完后需要逐层释放
for(int i = 0; i < rows; ++i) {
    delete[] matrix[i];
}
delete[] matrix;

2. 函数参数传递指针

二级指针可以作为函数参数,用于修改指针本身(即让指针指向新的地址)或通过指针修改指针所指向的数据。

void swapIntPointers(int **ptr1, int **ptr2) {
    int *temp = *ptr1;
    *ptr1 = *ptr2;
    *ptr2 = temp;
}

int main() {
    int a = 1, b = 2;
    int *ptra = &a, *ptrb = &b;
    swapIntPointers(&ptra, &ptrb);
    // 此时 ptra 和 ptrb 的指向已经交换
}

3. 动态内存管理

在复杂的内存管理操作中,二级指针可以用来传递内存地址,从而在函数内部动态分配或释放内存。

void allocateIntArray(int **array, int size) {
    *array = new int[size];
}

void deallocateIntArray(int **array) {
    delete[] *array;
    *array = nullptr;
}

4. 指针数组与数组指针

虽然概念上容易混淆,但二级指针也可以视为“指针数组”的指针,或指向“数组指针”的指针,这在处理多维数据结构或复杂的数据传递时非常有用。

9.空指针和野指针

在C/C++编程中,空指针和野指针是两种需要特别注意的指针状态,它们都可能导致程序运行时错误,包括但不限于程序崩溃、未定义行为或数据损坏。

1.空指针(NULL/nullptr)

  • 定义:空指针是指指向地址0的指针,它是表示指针不指向任何有效内存的一种约定。在C++中推荐使用nullptr,而在C语言中通常使用NULL(宏定义,其实际值可能为0或(void*)0)。
  • 用途:初始化指针为nullptr可以避免未初始化指针的误用,同时也便于检查指针的有效性。在条件判断中,如果指针是nullptr,则不执行对指针的解引用或其他操作。
  • 安全实践:在函数未能成功分配内存、返回指针失败或初始化阶段,应将指针设为nullptr

2.野指针

  • 定义:野指针指的是未初始化的指针,它可能指向内存中的任意位置,这个位置可能是无效的、已被分配给其他变量的,或者属于程序不可访问的区域。
  • 风险:由于野指针的值是不确定的,对野指针进行解引用或修改其指向的内存可能导致程序崩溃、数据损坏或安全漏洞。
  • 原因:野指针通常是因为程序员忘记初始化指针变量导致的。此外,局部变量作为指针使用并在其作用域结束后续存其地址也会形成野指针。
  • 避免策略:始终初始化指针,即使是将其设为nullptr。使用智能指针(C++中)可以自动管理内存,减少野指针的出现。

10.一维数组和指针

在C/C++中,一维数组和指针之间存在着紧密的关系,它们在很大程度上可以互换使用,这为编程提供了灵活性。以下是它们之间的主要关联点:

2.数组名作为指针

  • 数组名:在一个数组声明中,数组名实际上可以被看作是一个指向数组首元素的指针常量。这意味着数组名可以被用在任何需要指针的地方,但它自己并不可以被改变,不能用于重新指向另一个数组的首地址。
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 合法,arr可以被当作指针使用

3.指针遍历数组

  • 指针算术:使用指针可以方便地遍历数组元素。增加指针会使其指向数组的下一个元素。
for(int *p = arr; p != arr + 5; ++p) {
    printf("%d ", *p);
}

4.解引用和数组索引

  • 等价性:解引用指针和通过数组索引访问元素在效果上是相同的,两者都访问数组的特定位置。
int valueAtIndexZero = *arr; // 等同于 arr[0]

5.数组作为函数参数

  • 数组退化:当数组作为函数参数传递时,它会退化为指向其首元素的指针,因此在函数内部无法直接获得数组的长度信息。通常需要额外传递数组长度作为参数。
void printArray(int *arr, int size) {
    for(int i = 0; i < size; ++i) {
        printf("%d ", arr[i]);
    }
}

11.用new动态创建一维数组

在C++中,使用new操作符动态创建一维数组的过程涉及分配连续的内存空间,并返回一个指向该数组第一个元素的指针。下面是如何使用new来动态创建一维整型数组的例子:

#include <iostream>

int main() {
    // 动态创建一个包含10个整数的数组
    int *array = new int[10];

    // 初始化数组元素
    for(int i = 0; i < 10; ++i) {
        array[i] = i * 2; // 示例:给每个元素赋值为它的索引乘以2
    }

    // 打印数组元素
    for(int i = 0; i < 10; ++i) {
        std::cout << array[i] << " ";
    }
    std::cout << std::endl;

    // 释放内存
    delete[] array; // 注意使用[],因为是释放数组内存

    return 0;
}

这段代码首先使用new int[10]动态地在堆上分配足够存储10个整数的空间。然后,通过简单的循环初始化数组元素,并打印出来。最后,使用delete[] array释放之前分配的内存。重要的是要注意,当使用new[]分配数组时,必须使用delete[]来释放内存,而不是单独的delete,以避免内存泄漏或程序崩溃。

12函数指针和回调函数

1.函数指针

函数指针是一种特殊类型的指针,它存储的是函数的地址而不是数据的地址。这使得你可以在程序中像传递其他数据一样传递函数,使得函数可以在运行时被动态地选择和调用。函数指针的声明需要指定函数的返回类型和参数列表。

// 包含标准输入输出库
#include <iostream>

// 使用std命名空间,避免在代码中多次使用std::前缀
using namespace std;

// 定义一个打印函数,无参数,无返回值
// 该函数用于输出字符串"hello world!"到标准输出
void print(){
    cout << "hello world!" << endl;
}

// 程序入口主函数
int main(){
    // 定义一个函数指针p,指向无参数无返回值的函数
    void(*p)();
    // 将函数print的地址赋值给函数指针p
    p = print;
    // 直接调用函数print输出"hello world!"
    print();
    // 通过函数指针p调用函数print,再次输出"hello world!"
    p();
    // 程序正常结束,返回0
    return 0;
}

2.回调函数

回调函数是一种设计模式,它允许我们向某个函数或对象传递一个函数指针,这个函数将在未来的某个时刻由该接收方调用,通常是作为响应某种事件或条件的一部分。回调函数常用于事件驱动编程、异步编程和各种API中,以提供定制化的处理逻辑。

// 包含输入输出流的头文件
#include <iostream>

// 使用标准命名空间,简化IO操作
using namespace std;

// 定义一个函数,模拟小猫说话
// 该函数不接受任何参数,也不返回任何值
void catSpeak(){
    cout << "小猫说话" << endl;
}

// 定义一个函数,模拟小狗说话
// 该函数不接受任何参数,也不返回任何值
void dogSpeak(){
    cout << "小狗说话" << endl;
}

// 定义一个函数,用来教授动物说话
// 该函数接受一个函数指针作为参数,该指针指向一个不接受参数且不返回任何值的函数
// 函数通过调用给定的函数指针来模拟动物说话
void animalSpeak(void(*ptr)()){
    cout << "学习说话" << endl;
    ptr();
}

// 程序的入口点
int main(){
    // 教小猫说话
    animalSpeak(catSpeak);
    // 教小狗说话
    animalSpeak(dogSpeak);
    return 0;
}

这段代码定义了两个模拟动物说话的函数catSpeakdogSpeak,它们不接受任何参数,也不返回任何值,只是通过输出流cout打印出相应的字符串。另外,还定义了一个animalSpeak函数,它接受一个函数指针作为参数,该指针指向一个不接受参数且不返回任何值的函数。animalSpeak函数首先打印出"学习说话"的字符串,然后通过调用给定的函数指针来模拟动物说话。在main函数中,分别调用animalSpeak函数来教小猫和小狗说话。

相关推荐

  1. C++:智能指针

    2024-07-14 19:38:04       31 阅读
  2. C/C++指针&智能指针

    2024-07-14 19:38:04       17 阅读
  3. boost智能指针

    2024-07-14 19:38:04       54 阅读
  4. 智能指针unique_ptr

    2024-07-14 19:38:04       59 阅读
  5. C++ 智能指针

    2024-07-14 19:38:04       49 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-14 19:38:04       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-14 19:38:04       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-14 19:38:04       58 阅读
  4. Python语言-面向对象

    2024-07-14 19:38:04       69 阅读

热门阅读

  1. Spring Boot

    2024-07-14 19:38:04       14 阅读
  2. pnpm 如何安装指定版本

    2024-07-14 19:38:04       25 阅读
  3. Feedback

    2024-07-14 19:38:04       16 阅读
  4. 数据库崩溃时事务的恢复机制

    2024-07-14 19:38:04       16 阅读
  5. 怎样获取openid?

    2024-07-14 19:38:04       16 阅读