C++ 对象特性

发布于:2025-06-19 ⋅ 阅读:(21) ⋅ 点赞:(0)

1. 构造函数和析构函数

构造函数:与类同名,无返回值。对象创建时自动调用,用于初始化成员变量。
析构函数:以~类名命名,无参数无返回值。对象销毁时自动调用,用于资源释放。

示例:

class Demo {
public:
    Demo() { std::cout << "无参构造" << std::endl; }
    Demo(int a, double b) { std::cout << "有参构造" << std::endl; }
    Demo(const Demo& other) { std::cout << "拷贝构造" << std::endl; }
    Demo(Demo&& other) { std::cout << "移动构造" << std::endl; }
    ~Demo() { std::cout << "析构" << std::endl; }
};

2. 构造成员函数的分类及调用

类型 说明 示例语法
无参构造函数 没有参数或所有参数都有默认值 Demo();
有参构造函数 带有一个或多个参数 Demo(int a, double b);
拷贝构造函数 用同类型对象初始化新对象,参数为const 类名& Demo(const Demo& other);
移动构造函数 用右值对象初始化新对象,参数为类名&&(C++11及以后) Demo(Demo&& other);
默认构造函数 编译器自动生成的无参构造函数 Demo();(未自定义时自动生成)

无参构造函数:

Demo d1;         // 直接定义
Demo d2();       // 注意:这是函数声明,不是对象定义
Demo d3{};       // C++11列表初始化
Demo* p = new Demo; // 动态分配

有参构造函数:

Demo d4(10, 3.14);    // 直接初始化
Demo d5 = Demo(20, 2.71); // 显式构造
Demo d6{30, 1.23};    // C++11列表初始化
Demo* p2 = new Demo(40, 0.99);

拷贝构造函数:

Demo d7 = d4;         // 拷贝初始化
Demo d8(d4);          // 直接初始化
Demo* p3 = new Demo(d4);

移动构造函数:

Demo d9 = std::move(d4); // 移动初始化
Demo d10(std::move(d5));

注意事项:
• 如果自定义了有参构造函数,编译器不再自动生成无参构造函数,需手动补充。
• 拷贝构造函数常用于对象按值传递、返回时。
• 移动构造函数用于高效转移资源(如动态内存、容器等)。
• 构造函数可以重载,参数列表不同即可。

3. 拷贝构造函数调用时机

拷贝构造函数用于用一个对象初始化另一个同类型对象,常见调用时机:
• 用已存在对象初始化新对象:Demo d2 = d1;
• 函数参数按值传递对象
• 函数返回对象(按值返回)

示例:

class Demo {
public:
    Demo(const Demo& other) { /* ... */ }
};

4. 构造函数调用规则

• 若未定义任何构造函数,编译器自动生成无参构造、拷贝构造、析构函数。
• 一旦自定义了构造函数,编译器不再自动生成无参构造。
• 构造函数可重载。

5. 深拷贝与浅拷贝

• 浅拷贝:成员变量按值复制,指针成员只复制地址,易导致多次释放同一内存。
• 深拷贝:指针成员分配新内存并复制内容,避免资源冲突。

class Demo {
    char* data;
public:
    Demo(const Demo& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }
    ~Demo() { delete[] data; }
};

6. 初始化列表

• 构造函数可以使用初始化列表对成员变量进行初始化,语法为: 成员1(值1), 成员2(值2)。
• 成员变量在对象创建时就被初始化,而不是先默认构造再赋值,避免了多次构造和赋值,提高效率。
• 对于内置类型,初始化列表和构造函数体赋值效果类似,但对于类类型,初始化列表可以直接调用目标构造函数。

示例:

class Demo {
    int a;
public:
    Demo(int x) : a(x) {} // 直接初始化a
    // Demo(int x) { a = x; } // 先默认初始化a,再赋值,效率低
};

• const成员和引用成员必须在对象创建时初始化,不能在构造函数体内赋值,否则编译报错,初始化列表是唯一的初始化方式。

示例:

class Demo {
    const int a;
    int& ref;
public:
    Demo(int x, int& y) : a(x), ref(y) {} // 正确
    // Demo(int x, int& y) { a = x; ref = y; } // 错误,编译不通过
};

• 如果类成员对象没有默认构造函数,必须通过初始化列表显式调用其有参构造函数,否则编译失败。

class A {
public:
    A(int) {}
};
class B {
    A a;
public:
    B(int x) : a(x) {} // 正确
    // B(int x) { a = A(x); } // 错误,a没有默认构造函数
};

7. 类对象作为类成员

在C++中,一个类的成员变量可以是另一个类的对象,这种成员称为成员对象。这种设计常用于“整体-部分”关系(如:汽车包含发动机对象)。

构造和析构顺序:
• 构造顺序:先调用成员对象的构造函数,再调用自身的构造函数(成员对象按声明顺序初始化)。
• 析构顺序:先调用自身的析构函数,再调用成员对象的析构函数(析构顺序与构造相反)。

示例:

#include <iostream>

class Engine {
public:
    Engine() { std::cout << "Engine 构造" << std::endl; }
    ~Engine() { std::cout << "Engine 析构" << std::endl; }
};

class Car {
private:
    Engine engine; // 成员对象
public:
    Car() { std::cout << "Car 构造" << std::endl; }
    ~Car() { std::cout << "Car 析构" << std::endl; }
};

int main() {
    Car car;
    return 0;
}

输出顺序:

Engine 构造
Car 构造
Car 析构
Engine 析构

初始化成员对象:
• 如果成员对象没有默认构造函数,必须在外部类的初始化列表中显式初始化。

示例:

class A {
public:
    A(int) {}
};
class B {
    A a;
public:
    B(int x) : a(x) {} // 必须用初始化列表
};

8. 静态成员

静态成员(static member)包括静态成员变量和静态成员函数。它们属于类本身,而不是某个具体对象,所有对象共享同一份静态成员

静态成员变量:
• 用static关键字修饰,属于类级别,所有对象共享。
• 必须在类外进行一次性定义和初始化
• 可以通过类名或对象访问。

示例:

class Demo {
public:
    static int count; // 声明
    Demo() { ++count; }
};
int Demo::count = 0; // 类外定义和初始化

int main() {
    Demo a, b;
    std::cout << Demo::count << std::endl; // 输出2
    return 0;
}

静态成员函数:
• 用static修饰,不依赖具体对象。
• 只能访问静态成员变量其他静态成员函数,不能访问非静态成员。
• 可以通过类名或对象调用。

示例:

class Demo {
public:
    static int count;
    static void showCount() {
        std::cout << count << std::endl;
        // 不能访问非静态成员
    }
};
int Demo::count = 0;

int main() {
    Demo::showCount();
    return 0;
}

总结:
• 静态成员变量在程序生命周期内只有一份,节省内存。
• 静态成员函数没有this指针。
• 常用于计数、全局配置、工具函数等场景。

9. 成员变量和成员函数的存储

成员变量的存储:
• 成员变量(非静态)存储在每个对象的内存空间中。
每个对象都有自己独立的一份成员变量,互不影响。
• 对象的大小只与成员变量有关(不包括成员函数)。

示例:

class Demo {
    int a;
    double b;
};
Demo d1, d2;
// d1和d2各自有一份a和b
// sizeof(Demo) == sizeof(int) + sizeof(double)

成员函数的存储:
• 成员函数的代码只在内存中存一份,属于类本身,不属于任何对象。
• 所有对象共享同一份成员函数代码。
• 成员函数不占用对象的内存空间。

示例:

class Demo {
    int a;
public:
    void func() { /* ... */ }
};
Demo d1, d2;
// d1和d2都可以调用func(),但func()代码只存一份
// sizeof(Demo) 只计算a的大小

静态成员变量:
• 静态成员变量也不存储在对象内存中,而是存储在全局数据区,所有对象共享

总结:
• 成员变量:每个对象一份,存储在对象内存中。
• 成员函数:所有对象共享一份,存储在代码区,不占用对象空间。
• 静态成员变量:所有对象共享,存储在全局区,不占用对象空间。

10. this指针

this指针是每个非静态成员函数内部隐含的一个指针,指向当前调用该成员函数的对象本身。

基本作用:
• this的类型为ClassName* const,即指向当前对象的常量指针。
• 只能在非静态成员函数中使用,静态成员函数没有this指针。

主要用途:

  1. 区分成员变量和参数同名的情况:
    class Demo {
        int a;
    public:
        void setA(int a) {
            this->a = a; // 左边为成员变量,右边为参数
        }
    };
  1. 实现链式调用:
    class Demo {
        int a;
    public:
        Demo& setA(int a) {
            this->a = a;
            return *this; // 返回当前对象的引用
        }
    };
    // 用法:Demo d; d.setA(10).setA(20);
  1. 在成员函数中返回当前对象的指针或引用:
    class Demo {
    public:
        Demo* getThis() {
            return this;
        }
    };

注意事项:
• this指针指向调用成员函数的对象本身。
• 不能在静态成员函数中使用this。
• 通过this可以访问对象的所有成员。

总结:
this指针是C++对象模型的重要组成部分,主要用于区分同名、链式调用和返回自身等场景,提升代码的可读性和灵活性。

11. 空指针访问成员函数

可以通过空指针(nullptr)调用成员函数,但有以下注意事项:

允许的情况:
• 只要成员函数没有访问任何成员变量,通过空指针调用是安全的。
• 这是因为成员函数本质上是类的普通函数,只是多了一个隐式的this指针参数。

示例:

class Demo {
public:
    void foo() { std::cout << "ok" << std::endl; }
};

int main() {
    Demo* p = nullptr;
    p->foo(); // 合法,只要foo未访问成员变量
    return 0;
}

危险的情况:
• 如果成员函数访问了成员变量,通过空指针调用会导致未定义行为(通常是程序崩溃)。

示例:

class Demo {
    int a = 10;
public:
    void bar() { std::cout << a << std::endl; }
};

int main() {
    Demo* p = nullptr;
    p->bar(); // 非法,访问成员变量a会崩溃
    return 0;
}

原理说明:
• 调用成员函数时,编译器会将this指针传递给函数。
• 如果对象指针为nullptr,this就是nullptr。
• 只要成员函数内部不解引用this(即不访问成员变量/函数),就不会出错。

总结:
空指针可以调用成员函数,但前提是该函数不访问任何成员变量或成员函数。否则会导致未定义行为,实际开发中应尽量避免这种用法。

12. const修饰成员函数

const修饰成员函数表示该函数不会修改对象的成员变量(除mutable修饰的成员外),保证对象的常量性和接口的安全性。

基本语法:

class Demo {
    int a;
public:
    int getA() const { // const成员函数
        // a = 10; // 错误,不能修改成员变量
        return a;
    }
};

• const关键字写在成员函数参数列表后、函数体前。

作用:
• 保证成员函数不会修改对象状态,可用于const对象。
• 只能调用其他const成员函数,不能调用非const成员函数。

使用场景:
• 访问器(getter)等只读操作应声明为const成员函数。
• 使得对象可以作为const引用或const指针安全传递和使用。

示例:

void printDemo(const Demo& d) {
    std::cout << d.getA() << std::endl; // 只能调用const成员函数
}

this指针类型的变化:
• 普通成员函数中,this指针的类型是:ClassName* const this
• 在const成员函数中,this指针的类型变为:const ClassName* const this
• 也就是说,const成员函数内部,this指向的对象被视为常量,不能通过this修改成员变量(除mutable成员外)

编译器检查:
• 如果在const成员函数中修改成员变量(非mutable),编译器会报错,增强代码安全性。

总结:
const修饰成员函数可提升代码的健壮性和可读性,是C++类设计的良好实践。


网站公告

今日签到

点亮在社区的每一天
去签到