Skip to content

5 数据抽象和类II

内存布局与对象

正文

  • 函数实现,库实现,字符串等资源,不可改
char *s1 - "Literal"; //文字在代码区,仅分配了字符指针
// s1[0] = 'I' //不能修改
char s2[] = "Initial Literal"; //分配数组空间
// s2可以被修改
Point *po = &((Point){2, 3}); //分配结构空间且指针
// 取临时对象的地址给po,语句执行完毕之后临时对象就没了

静态变量(对象)

  • main执行前初始化数据
  • 用零初始化

栈(Stack)

  • 函数参数,自动变量(对象)

堆(Heap)

  • 动态变量(对象),由stdlib.h管理

总结

从低地址到高地址:

  1. 代码区,存放函数代码,是只读的
  2. 全局/静态区,存放数据(字符串字面量、已初始化/未初始化的全局变量和静态变量)
  3. 堆区,程序员手动分配
  4. 栈区,编译器自动管理的函数被调用时的局部变量,函数返回时这些变量自动销毁

  5. 上述代码里的s1指向的字符串字面量在静态区,不可修改,s2整个数组在栈上,它的内容是"Initial Literal"的一个副本。

C语言动态对象管理

  • 必须#include<stdlib.h>
  • 申请空间void * malloc(size_t)
  • 释放空间void free(void *)
Point *p1 = malloc(sizeof(Point) * 10);

Point (*p2)[2][3] = malloc(sizeof(Point) * 6);
//指向2*3数组的指针

int n = 2, m = 3;
Point **p3 = malloc(sizeof(Point*) * n);
//指针的指针
for(size_t i = 0; i < n; i++){
    p3[i] = malloc(sizeof(Point) * m);
}

free(p1);
free(p2);

for(size_t i = 0; i < n; i++){
    free(p3[i]);
}
free(p3);//先释放行数组,再释放指针的指针

new运算符

  1. 指针 = new 类型名
  2. 指针 = new 类型名(初始化参数)
  3. 指针 = new 类型名[数组长度]

  4. 类型可以为基本类型,也可以为类(class)类型,如果为类,则初始化参数相当于将实际参数传递给该类的构造函数。

  5. new运算符返回一个该类型的指针,指向分配到的内存空间
  6. 若内存分配失败,抛出异常,不是NULL

delete运算符

  1. delete 变量名
  2. delete[] 数组名

  3. 如果分配了数组但是用delete p的方式释放,没有用[],会导致分配的数组没有完全释放

  4. 释放之后指针仍然指向原来的地址,但指针已无效,重复释放将出错
  5. delete对象指针会调用该对象的析构函数

内存泄漏

指new的空间失去了指针或引用永远无法释放,导致资源耗尽。

int *p = new int(7);
p = nullptr; // 内存泄漏
void f(){
    int *p = new int(7);
}
//离开作用域,指针没有了,内存泄漏

显式定义默认函数

当我们声明有参构造函数时,编译器不会创建默认构造函数。=default是令编译器为某个类生成特殊成员函数或比较运算符的显式指令(C++11)。特殊成员函数包括:

  • 默认构造函数T()
  • 析构函数~T()
  • 拷贝构造函数T(const T&)
  • 赋值运算operator=(const T&)

=default必须在1函数声明后,让编译创建该构造函数。

class A
{
public:
    A(int x){
        cout << "This is a parameterized constructor";
    }
    A() = default;
};

弃置函数

在 C++ 中,=delete 是一个用于显式禁止某个函数被调用的语法特性。你可以把它理解为给函数贴上一个“禁止通行”的标签。一旦某个函数被标记为 =delete,任何尝试调用它的代码都会在编译期直接报错,从而有效防止误用。

请注意,删除的函数是隐式内联的,这一点非常重要。删除的函数定义必须是函数的首次声明

成员初始化器

在 C++ 中,“成员初始化器”通常指的是构造函数初始化列表。是一种在构造函数体执行之前,直接初始化类成员变量的语法机制。

语法结构

成员初始化器位于构造函数的参数列表之后,构造函数体之前,以冒号 : 开头,后面跟着一系列以逗号分隔的成员初始化项。

class MyClass {
private:
    int x;
    int y;
    std::string name;

public:
    // 构造函数
    // x(a) 和 name(n) 就是成员初始化器
    MyClass(int a, const std::string& n) : x(a), name(n) {
        // 构造函数体
        y = 0; // 这是在函数体内赋值,不是初始化
    }
};

为什么要使用成员初始化器

性能优化:

如果你在构造函数体内使用赋值操作(例如 x = a;),对于非内置类型(如 std::string 或自定义类),编译器会执行以下步骤: 1. 默认构造:先调用成员的默认构造函数(例如创建一个空字符串)。 2. 赋值:再调用赋值运算符,将新值赋给它。

这相当于做了两次工作。而使用成员初始化器,编译器会直接调用带参数的构造函数进行初始化,只执行一次,效率更高。

强制需求:

在以下三种情况下,你必须使用成员初始化器,否则代码无法通过编译:

  • const 成员变量:常量一旦创建就不能被修改,因此必须在创建时(即初始化阶段)赋值,不能在函数体内赋值。
  • 引用成员变量:引用必须在定义时绑定到一个有效的对象,不能先定义空引用再赋值。
  • 没有默认构造函数的类成员:如果成员是一个类对象,且该类没有默认构造函数(即必须传参才能构造),则必须在初始化列表中显式传递参数。

初始化顺序

这是一个非常容易被忽视但至关重要的规则:

成员的初始化顺序,完全取决于它们在类中声明的顺序,而与初始化列表中书写的顺序无关。

class Example {
private:
    int a;
    int b;

public:
    // 看起来是先初始化 b,再初始化 a
    // 但实际上:a 先被初始化(因为它在类中先声明),b 后被初始化
    Example(int val) : b(val), a(b) { 
        // 此时 a 的值是未定义的!因为 b 还没初始化,a 就尝试用 b 的值
    }
};
为了保证代码的可读性和避免逻辑错误,初始化列表中的书写顺序最好与类成员声明的顺序保持一致

对象成员初始化器

对象成员初始化器就是在创建对象时,用 {} 直接给成员变量赋值的语法糖。

语法糖(Syntactic Sugar),又称糖衣语法,是由英国计算机科学家彼得·约翰·兰达提出的术语,指编程语言中添加的简化语法结构。这类语法不改变语言功能,但能提升代码可读性并减少错误率,例如C语言中a[i]等价于指针表达式*(a+i)。

举例

假设有个类:

struct Student {
    int id;
    string name;
};

对象成员初始化器创建对象:

Student s = {1001, "张三"};
或者:
Student s{1001, "张三"}; // C++11 统一初始化

类数组

struct Student {
    int id;
    string name;
};

// 类数组,直接用 {} 初始化每个对象
Student arr[] = {
    {101, "小明"},
    {102, "小红"},
    {103, "小李"}
};
或者
Student arr[] {
    {101, "小明"},
    {102, "小红"},
    {103, "小李"}
};

只要你的类/结构体满足:

  • 没有自定义构造函数
  • 没有 private 成员(或成员可访问)
  • 是简单的聚合类(aggregate)

就可以用 对象成员初始化器 初始化数组。

与new结合

申请数组:new 类型[]{初始化列表}

int *p;
int Length, i;

cin >> Length;
p = new int[Length]{1,3,5};

用new给类分配内存时:

  • 如果后面没有初始化列表,会调用无参构造(若为默认生成的无参构造,会初始化为不确定值)
  • 常数初始化列表(int[]double[]),用初始化列表赋值,元素不够时,后边补0。例如上述代码中Length大于3时。
  • 如果是类类型数组,不够 → 剩余元素用 默认构造函数(无参构造)初始化。
  • 如果给数组用了对象成员初始化器,数组所需的参数长度小于初始化序列长度,则抛出异常

特点

  • 成员声明顺序赋值
  • 适合简单结构体、类

nullptr

在现代 C++ (C++11 及以后) 中,nullptrNULL 的核心区别在于类型安全nullptr 是专门用于表示空指针的关键字,而 NULL 是一个代表整数值 0 的宏,容易在特定场景下引发歧义。