在 C++ 中,创建对象的方式主要有几种,包括栈上分配、堆上分配、使用智能指针、聚合初始化和列表初始化等。下面详细介绍这些方法:
1. 栈上分配(自动分配)
在栈上创建对象是最常见和高效的方式。对象的生命周期与作用域相关,当作用域结束时,对象会被自动销毁。
#include <iostream>
class A {
public:
A() {
std::cout << "A constructor called" << std::endl;
}
~A() {
std::cout << "A destructor called" << std::endl;
}
};
int main() {
A a; // 在栈上创建一个 A 对象
return 0;
}
栈上分配的对象在作用域结束时会自动调用析构函数。析构函数用于执行清理工作,如释放动态分配的内存、关闭文件、断开网络连接等。如果你的类管理了一些需要在对象销毁时进行清理的资源,那么你应该编写析构函数。如果没有显式定义析构函数,C++ 会提供一个默认的析构函数。是否需要编写析构函数取决于你的类是否管理了需要在对象销毁时进行清理的资源。如果你的类没有这样的需求,那么默认的析构函数就足够了。
2. 堆上分配(动态分配)
使用 new
关键字在堆上动态分配内存。对象的生命周期由程序员手动管理,需要使用 delete
释放内存。
#include <iostream>
class A {
public:
A() {
std::cout << "A constructor called" << std::endl;
}
~A() {
std::cout << "A destructor called" << std::endl;
}
};
int main() {
A* a=new A() // 在堆上创建一个 A 对象
delete a; // 释放内存
return 0;
}
A* a
:将星号*
紧跟在类型后面,表示a
是一个指向A
类型的指针。A *a
:将星号*
放在变量名前面,同样表示a
是一个指向A
类型的指针。
new,delete的两种搭配,不能交换。
A* a = new A(); // 在堆上分配一个 A 对象
delete a; // 释放内存并调用析构函数
A* array = new A[5]; // 在堆上分配一个包含 5 个 A 对象的数组
delete[] array; // 释放数组内存并调用每个对象的析构函数
确保每次使用 new
分配的内存都在适当的时候使用 delete
释放,否则会导致内存泄漏。
为了更好地管理动态分配的内存,推荐使用智能指针(如 std::unique_ptr
和 std::shared_ptr
),以避免内存泄漏和双重删除等问题。
#include <iostream>
#include <memory>
class A {
public:
A() {
std::cout << "A constructor called" << std::endl;
}
~A() {
std::cout << "A destructor called" << std::endl;
}
};
int main() {
std::unique_ptr<A> a = std::make_unique<A>(); // 使用 unique_ptr 管理堆上的 A 对象
std::unique_ptr<A[]> array = std::make_unique<A[]>(5); // 使用 unique_ptr 管理堆上的 A 数组
return 0;
}
3. 使用智能指针
智能指针(如 std::unique_ptr
和 std::shared_ptr
)可以帮助管理动态分配的内存,避免内存泄漏。
1) std::unique_ptr
#include <iostream>
#include <memory>
class A {
public:
A() {
std::cout << "A constructor called" << std::endl;
}
~A() {
std::cout << "A destructor called" << std::endl;
}
};
int main() {
std::unique_ptr<A> a = std::make_unique<A>(); // 使用 unique_ptr 管理堆上的 A 对象
return 0;
}
2) std::shared_ptr
#include <iostream>
#include <memory>
class A {
public:
A() {
std::cout << "A constructor called" << std::endl;
}
~A() {
std::cout << "A destructor called" << std::endl;
}
};
int main() {
std::shared_ptr<A> a = std::make_shared<A>(); // 使用 shared_ptr 管理堆上的 A 对象
return 0;
}
4. 聚合初始化
聚合初始化用于初始化具有公共访问权限的数据成员的类或结构体。适用于没有用户定义的构造函数的类。
#include <iostream>
struct A {
int x;
double y;
};
int main() {
A a = {1, 2.5}; // 聚合初始化
std::cout << "x: " << a.x << ", y: " << a.y << std::endl;
return 0;
}
5. 列表初始化
列表初始化(也称为统一初始化)是从 C++11 开始引入的一种初始化方式,可以用于各种类型的对象。
#include <iostream>
class A {
public:
A(int x, double y) : x(x), y(y) {
std::cout << "A constructor called" << std::endl;
}
int x;
double y;
};
int main() {
A a{1, 2.5}; // 列表初始化
std::cout << "x: " << a.x << ", y: " << a.y << std::endl;
return 0;
}
6. 静态对象
静态对象在程序启动时创建,在程序结束时销毁。它们的生命周期与整个程序的生命周期相同。
#include <iostream>
class A {
public:
A() {
std::cout << "A constructor called" << std::endl;
}
~A() {
std::cout << "A destructor called" << std::endl;
}
};
A globalA; // 全局静态对象
int main() {
static A staticA; // 局部静态对象
return 0;
}
静态对象的析构函数在程序结束时自动调用。对于全局静态对象,析构函数在 main
函数返回后调用。对于局部静态对象,析构函数在程序结束时调用。
>全局静态对象的初始化顺序是不确定的,特别是在多个文件中声明的全局静态对象之间。为了避免初始化顺序问题,可以使用局部静态对象或单例模式。
>局部静态对象在第一次进入其作用域时初始化,因此初始化顺序是确定的。
- 静态对象:
- 全局静态对象:在文件作用域内声明,程序启动时创建,程序结束时销毁。
- 局部静态对象:在函数内部声明,第一次进入其作用域时初始化,程序结束时销毁。
- 普通对象:
- 栈上对象:在函数或代码块的作用域内创建和销毁。
- 堆上对象:通过
new
动态分配,通过delete
释放。
- 静态成员变量:属于类而不是类的某个特定对象,所有对象共享同一个静态成员变量。
#include <iostream>
class A {
public:
A() {
count++;
std::cout << "A constructor called, count = " << count << std::endl;
}
~A() {
count--;
std::cout << "A destructor called, count = " << count << std::endl;
}
static int count; // 静态成员变量
};
int A::count = 0; // 静态成员变量的定义和初始化
int main() {
A a1;
A a2;
return 0;
}
类中声明,类外定义。(原因:因为静态成员变量为所有对象所共享唉,如果在类中定义,会导致多个源文件包含相同的定义,从而引发链接错误。每个源文件都会尝试为同一个静态成员变量分配存储空间,导致多重定义错误。)所以C++语法在类中声明静态成员变量,告诉编译器它所在的位置,在类外再进行内存的分配。
静态成员变量的存储空间需要在程序启动时分配,而类的声明只是告诉编译器该成员变量的存在。实际的存储空间分配需要在类外进行,这样可以确保只有一个定义,避免多重定义错误。
静态成员变量可以在定义时初始化,也可以在其他地方初始化。初始化时必须确保只有一处初始化,以避免重复初始化的问题。
静态成员变量可以通过类名直接访问,也可以通过类的实例访问。但无论哪种方式,访问的都是同一个变量。
综上:
- 声明:在类中声明静态成员变量,告诉编译器该成员变量的存在及其类型。
- 定义:在类外定义静态成员变量,分配实际的存储空间,并进行初始化。
- 分离的原因:避免多重定义错误,确保静态成员变量的唯一性。
- 访问:可以通过类名或类的实例访问静态成员变量,但访问的都是同一个变量。
总结
- 栈上分配:直接在栈上创建对象,最常见且高效。
- 堆上分配:使用
new
关键字在堆上动态分配内存,需要手动管理内存。 - 智能指针:使用
std::unique_ptr
或std::shared_ptr
管理动态分配的内存,避免内存泄漏。 - 聚合初始化:用于初始化具有公共访问权限的数据成员的类或结构体。
- 列表初始化:从 C++11 开始引入,适用于各种类型的对象。
- 静态对象:在程序启动时创建,在程序结束时销毁。
这些方法涵盖了 C++ 中创建对象的主要方式。