C与C++---指针、引用、结构体及内存管理

指针、引用、结构体及内存管理


1. 引用 (Reference) 和指针 (Pointer)

这是C++引入"引用"后最经典的对比。总的来说,引用是变量的"别名",而指针是存储变量地址的"变量"

指针 (Pointer)
  • 概念: 指针是一个变量,它本身有自己的内存地址,但它存储的内容是另一个变量的内存地址。通过这个地址,我们可以间接地访问或修改那个变量。

  • 核心特点:

    1. 可为空 (Nullable) : 指针可以被设置为 nullptr (或在旧代码中是 NULL),表示它不指向任何有效的内存地址。
    2. 可重新赋值 (Re-seatable): 指针可以在其生命周期内改变指向,让它先后指向不同的变量。
    3. 需要解引用 (Dereferencing) : 访问指针所指向的数据时,需要使用解引用操作符 *
    4. 拥有自己的内存: 指针变量本身占用内存空间(通常是4或8字节,取决于系统架构)。
    5. 支持指针算术: 可以对指针进行加减运算,使其在内存中前后移动,这对于数组操作非常重要。
  • 用法示例:

    cpp 复制代码
    int a = 10;
    int b = 20;
    
    int* ptr;         // 声明一个整型指针
    
    ptr = &a;         // 1. 指向变量 a 的地址
    cout << *ptr << endl; // 2. 解引用,输出 a 的值:10
    *ptr = 15;        // 3. 通过指针修改 a 的值,现在 a = 15
    
    ptr = &b;         // 4. 重新赋值,使其指向 b
    cout << *ptr << endl; // 输出 b 的值:20
    
    ptr = nullptr;    // 5. 设置为空指针
引用 (Reference)
  • 概念: 引用可以看作是一个已经存在的变量的别名。它不是一个新变量,不占用新的内存空间(或者说,它与原变量共享内存地址)。它提供了一种更简洁、更安全的方式来操作变量。

  • 核心特点:

    1. 不能为空 (Not Nullable) : 引用必须在声明时进行初始化,并且不能指向 nullptr。它必须始终关联一个有效的变量。
    2. 不可重新赋值 (Not Re-seatable): 一旦引用被初始化为某个变量的别名,它就不能再被改变为另一个变量的别名。对引用的赋值操作会直接修改它所引用的原始变量的值。
    3. 无需解引用: 使用引用时,语法上就像直接使用原始变量一样,不需要特殊的操作符。
    4. 不拥有自己的内存: 它只是一个别名,与原变量共享同一块内存。
    5. 不支持算术: 不能对引用进行算术运算。
  • 用法示例:

    cpp 复制代码
    int a = 10;
    int b = 20;
    
    int& ref = a;     // 1. 声明并初始化引用,ref 成为 a 的别名。必须在声明时初始化!
    // int& ref2;     // 错误!引用必须初始化。
    
    cout << ref << endl;  // 2. 直接使用,输出 a 的值:10
    ref = 15;         // 3. 修改 ref 就是修改 a,现在 a = 15
    
    ref = b;          // 4. 注意!这里不是让 ref 成为 b 的别名!
                      // 而是将 b 的值 (20) 赋给 ref 所引用的变量 a。
                      // 执行后,a 的值变为 20,ref 仍然是 a 的别名。
    
    cout << "a = " << a << endl; // 输出 a = 20
    cout << "ref = " << ref << endl; // 输出 ref = 20
总结
特性 指针 (Pointer) 引用 (Reference)
本质 存储地址的变量 变量的别名
空值 可以为 nullptr 必须引用一个已存在的变量,不能为空
初始化 可以在任何时候初始化 必须在声明时初始化
重定向 可以改变指向,指向另一个变量 一旦初始化,终生指向同一个变量
内存空间 自身占用内存空间 不占用独立内存空间(与原变量共享)
语法 使用 * 解引用,& 取地址 像普通变量一样使用
安全性 较低(空指针、野指针风险) 较高(不存在空引用,更安全)

: "什么时候该用指针,什么时候该用引用?"

  • 使用引用:

    1. 函数参数传递 : 当你希望在函数内部修改外部变量,或者为了避免大型对象的值拷贝开销时,引用是首选。尤其是 const 引用,它能避免拷贝且保证数据不被修改,非常高效和安全。

      cpp 复制代码
      void process_data(const BigObject& obj); // 高效且安全
    2. 函数返回值 : 当函数需要返回一个容器内的元素或一个类的成员时,可以返回引用以避免拷贝。但要极其小心,绝不能返回局部变量的引用,因为函数结束后局部变量会被销毁。

    3. 运算符重载 : 如重载 [], = 等操作符时,通常返回引用,以支持链式调用(如 a = b = c;)。

  • 使用指针:

    1. 表示"可能不存在" : 当一个变量可能指向一个对象,也可能什么都不指向(nullptr)时,必须用指针。例如,链表中的 next 节点指针。
    2. 动态内存管理 : 在堆(Heap)上创建的对象必须通过指针来管理(例如 new 返回的就是指针)。
    3. 数组操作: C/C++中数组名本质上就是指向首元素的指针,指针算术对于遍历和操作数组非常方便。

2.C与C++的struct对比

特性 C 中的 struct C++ 中的 struct
成员类型 只能包含数据成员 可以包含数据成员和成员函数(方法)
访问权限 无访问权限控制(所有成员都是公开的) 有访问权限控制 (public, private, protected),默认是 public
构造/析构函数 没有构造函数或析构函数 可以有构造函数和析构函数
继承 不支持继承 可以 从其他 structclass 继承
this 指针 没有 this 指针的概念 成员函数内部有 this 指针,指向当前对象
作为类型名 声明变量时必须带 struct 关键字 (如 struct Point p;),除非使用typedef struct 名直接就是类型名 (如 Point p;)

C 语言的实现 (struct 作为数据容器)

在C语言中,struct 用来定义数据结构,而操作这些数据的函数是全局的、与之分离的。

c 复制代码
#include <stdio.h>

// 1. 定义一个数据结构
struct Point {
    int x;
    int y;
};

// 2. 操作该数据结构的函数是全局的
void init_point(struct Point* p, int x, int y) {
    p->x = x;
    p->y = y;
}

void print_point(struct Point p) {
    printf("Point(%d, %d)\n", p.x, p.y);
}

int main() {
    // 3. 声明变量需要带 struct 关键字
    struct Point p1;

    // 4. 初始化和操作都是通过外部函数完成
    init_point(&p1, 10, 20);
    print_point(p1); // 输出: Point(10, 20)

    return 0;
}

特点总结:

  • 数据(struct Point)和行为(init_point, print_point)是分离的,这是典型的面向过程编程。

  • 必须使用 struct Point p1; 来声明变量。为了简化,C程序员通常会用 typedef

    c 复制代码
    typedef struct Point {
        int x;
        int y;
    } Point; // 现在可以用 Point p1; 来声明了
C++ 语言的实现 (struct 作为轻量级对象)

在C++中,struct 可以将数据和操作数据的行为封装在一起。

cpp 复制代码
#include <iostream>

// 1. 定义一个轻量级对象
struct Point {
    // 数据成员
    int x;
    int y;

    // 2. 成员函数 (行为)
    // 构造函数
    Point(int x_val, int y_val) {
        x = x_val;
        y = y_val;
    }

    void print() {
        // 成员函数可以直接访问成员变量
        std::cout << "Point(" << x << ", " << y << ")" << std::endl;
    }
};

int main() {
    // 3. 声明变量时不需要 struct 关键字
    Point p1(10, 20); // 4. 自动调用构造函数进行初始化

    // 5. 直接调用对象的成员函数
    p1.print(); // 输出: Point(10, 20)

    // 成员默认是 public,可以直接访问
    p1.x = 30;
    p1.print(); // 输出: Point(30, 20)

    return 0;
}

特点总结:

  • 数据和行为被封装在了一起,这是面向对象编程的基本思想。
  • 拥有构造函数,使得对象的初始化更安全、更便捷。
  • 可以直接使用 Point p1; 进行声明。

3. structclass

在C++中,structclass 的功能几乎完全相同,唯一的核心区别 在于默认的访问权限和继承权限

特性 struct class
默认成员访问权限 public private
默认继承方式 public private
详细解释
  1. 默认成员访问权限:

    • 在一个 struct 中,如果你不显式指定 public:, private:, 或 protected:,那么其所有成员(包括数据和方法)默认都是 public 的。
    • 在一个 class 中,默认是 private 的。
    cpp 复制代码
    struct MyStruct {
        int x; // 默认是 public
        void print() {} // 默认是 public
    };
    
    class MyClass {
        int x; // 默认是 private
        void print() {} // 默认是 private
    };
    
    int main() {
        MyStruct s;
        s.x = 10; // OK
    
        MyClass c;
        // c.x = 10; // 错误! 'x' is private
    }
  2. 默认继承方式:

    • 当一个 struct 继承自另一个 structclass 时,默认的继承方式是 public 继承。
    • 当一个 class 继承时,默认是 private 继承。
    cpp 复制代码
    class Base { public: int y; };
    
    struct DerivedStruct : Base { /* y is public here */ };
    class DerivedClass : Base { /* y is private here */ };
使用约定
  • 使用 struct:

    • 当你只是想将一堆数据聚合在一起,作为一个数据容器时。
    • 这些数据成员之间没有复杂的逻辑关系或不变量(invariant)需要维护。
    • 成员默认是公开的,外部可以直接访问和修改。
    • 通常称之为 POD (Plain Old Data) 类型。
    • 例子 : Point { double x, y; }, Color { int r, g, b; }
  • 使用 class:

    • 当你需要封装数据,隐藏内部实现细节时。
    • 需要通过 public 接口(成员函数)来控制对 private 成员的访问。
    • 需要维护类的不变量 (例如,一个 Date 类的 day 成员必须在1到31之间)。
    • 当你打算使用继承和多态等面向对象的特性时。
    • 例子 : DatabaseConnection, HttpRequest, Car 等具有复杂行为和状态的对象。

总结 : 用 struct 来表示数据 ,用 class 来表示拥有复杂行为和封装状态的对象。这是一种代码风格,能让其他开发者更快地理解你的意图。


3. new/deletemalloc/free

这两组函数都用于在堆(Heap)上动态分配和释放内存,但它们属于不同的"时代"和技术范畴。

  • malloc / free : C语言标准库函数,位于 <cstdlib> (或 <stdlib.h>)。
  • new / delete: C++的运算符。
核心区别
特性 malloc / free new / delete
来源 C标准库函数 C++运算符
类型安全 不安全malloc 返回 void*,需要手动强制类型转换。 类型安全new 返回对应类型的指针,无需转换。
构造/析构 不会调用对象的构造函数和析构函数。 new 在分配内存后会调用构造函数delete 在释放内存前会调用析构函数
内存大小 需要手动计算所需字节数,如 malloc(10 * sizeof(int)) 编译器自动计算所需大小,如 new int[10]
数组处理 free 对单个对象和数组使用方式相同。 单个对象用 delete,数组用 delete[]混用是严重错误
重载 不可重载。 可以被类重载,实现自定义的内存分配策略。
异常处理 分配失败时返回 NULL 分配失败时默认抛出 std::bad_alloc 异常。
用法示例与陷阱
cpp 复制代码
#include <iostream>
#include <cstdlib>

class MyObject {
public:
    MyObject() { std::cout << "Constructor called" << std::endl; }
    ~MyObject() { std::cout << "Destructor called" << std::endl; }
};

int main() {
    // --- C 风格 ---
    std::cout << "Using malloc/free:" << std::endl;
    MyObject* obj1 = (MyObject*)malloc(sizeof(MyObject)); // 1. 手动计算大小,强制转换
                                                         // 2. 注意:构造函数没有被调用!
    free(obj1);                                          // 3. 注意:析构函数没有被调用!

    std::cout << "\n--- C++ 风格 ---" << std::endl;
    MyObject* obj2 = new MyObject(); // 1. 类型安全,自动计算大小
                                     // 2. 构造函数被自动调用
    delete obj2;                     // 3. 析构函数被自动调用

    std::cout << "\n--- C++ 数组 ---" << std::endl;
    MyObject* arr = new MyObject[3]; // 调用3次构造函数
    delete[] arr;                    // 调用3次析构函数。如果写成 delete arr; (错误!),只会调用一次析构,导致内存泄漏!
}

相关推荐
怪力左手3 小时前
地图下载工具
开发语言·ios·swift
wjs20243 小时前
C 标准库 - `<time.h>`
开发语言
浪飘4 小时前
golang读写锁
开发语言·数据库·golang
2301_789015624 小时前
算法与数据结构——排序算法大全
c语言·开发语言·数据结构·c++·算法·排序算法·visual studio
学习编程的Kitty4 小时前
JavaEE初阶——多线程(1)初识线程与创建线程
java·开发语言·java-ee
勤奋菲菲4 小时前
Egg.js 完全指南:企业级 Node.js 应用框架
开发语言·javascript·node.js
蒂法就是我5 小时前
java集合类的底层类是哪个
java·开发语言
无限进步_5 小时前
冒泡排序的多种实现方式详解
c语言·数据结构·c++·算法
老花眼猫5 小时前
可自动求解的魔法游戏程序(C语言)
c语言·经验分享·青少年编程·课程设计