(四)C++的类与动态内存分配
一.动态内存
在C++中,动态内存是指程序在运行时根据需要分配和释放的内存。与静态内存(编译时分配)和栈内存(函数调用时自动分配和释放)不同,动态内存由程序员显式管理。这为程序提供了更大的灵活性,但也带来了更大的责任,因为不正确的内存管理可能导致内存泄漏、悬挂指针和缓冲区溢出等问题。以下是对C++动态内存管理的详细介绍,包括内存分配方式、内存释放、常见问题以及现代C++中的智能指针
内存分配方式
a. 使用new和delete
new:用于在堆上分配内存,并可选择性地初始化对象。
delete:用于释放通过new分配的内存。
示例:
#include <iostream>int main() {// 分配单个对象int* ptr = new int(10);//初始化为10std::cout << *ptr << std::endl; // 输出: 10// 分配数组int* arr = new int[5];//分配5个int空间,arr指向第一个元素地址for(int i = 0; i < 5; ++i) {arr[i] = i * 2;std::cout << arr[i] << " ";}std::cout << std::endl;// 释放内存delete ptr;delete[] arr;return 0;
}
b. 使用malloc和free
malloc:用于在堆上分配未初始化的内存,返回void*指针。
free:用于释放通过malloc分配的内存
示例:
#include <iostream>
#include <cstdlib>int main() {// 分配内存int* ptr = (int*)malloc(sizeof(int));if(ptr != nullptr) {*ptr = 10;std::cout << *ptr << std::endl; // 输出: 10free(ptr); // 释放内存}// 分配数组int* arr = (int*)malloc(5 * sizeof(int));if(arr != nullptr) {for(int i = 0; i < 5; ++i) {arr[i] = i * 2;std::cout << arr[i] << " ";}std::cout << std::endl;free(arr); // 释放内存}return 0;
}
malloc和free不调用构造函数和析构函数,因此不适用于C++对象的内存管理。应优先使用new和delete。
内存释放
正确释放动态分配的内存对于防止内存泄漏至关重要:
释放单个对象:使用delete。
释放数组:使用delete[]。
避免双重释放:确保每个new对应一个delete,每个new[]对应一个delete[]。
示例:
int* ptr = new int(10);
// 使用ptr
delete ptr; // 正确释放
常见问题
a. 内存泄漏
定义:分配的内存没有被释放,导致内存资源被浪费
解决方法:确保每个new都有对应的delete,或者使用智能指针来自动管理内存。
void leakyFunction() {int* ptr = new int(10);// 没有调用 delete ptr;
}
b. 悬挂指针
定义:指针指向的内存已经被释放,但指针仍然引用该内存地址。
解决方法:在释放内存后,将指针设为nullptr,并在使用前进行检查
int* ptr = new int(10);
delete ptr;
*ptr = 5; // 未定义行为
c. 缓冲区溢出
定义:访问超出分配内存范围的内存区域。
解决方法:始终检查数组索引,确保在有效范围内访问内存。
int* arr = new int[5];
arr[5] = 10; // 未定义行为,最大arr[4]
delete[] arr;
智能指针
为了简化动态内存管理,C++11引入了智能指针(Smart Pointers),它们自动管理内存,避免内存泄漏和悬挂指针等问题。主要的智能指针包括:
a. std::unique_ptr
特点:独占所有权,不能被复制,只能被移动。
用途:用于管理独占所有权的资源。
示例:
#include <memory>
#include <iostream>int main() {std::unique_ptr<int> ptr = std::make_unique<int>(10);std::cout << *ptr << std::endl; // 输出: 10// 不需要手动调用 deletereturn 0;
}
b. std::shared_ptr
特点:共享所有权,多个shared_ptr可以指向同一个对象,内部使用引用计数。
用途:用于需要多个所有者共享资源的场景。
示例:
#include <memory>
#include <iostream>int main() {std::shared_ptr<int> ptr1 = std::make_shared<int>(10);{std::shared_ptr<int> ptr2 = ptr1;std::cout << *ptr2 << std::endl; // 输出: 10}std::cout << *ptr1 << std::endl; // 输出: 10return 0;
}
c. std::weak_ptr
特点:不拥有对象,不增加引用计数,用于解决shared_ptr循环引用的问题。
用途:用于需要观察对象但不希望拥有对象的情况。
示例:
#include <memory>
#include <iostream>int main() {std::shared_ptr<int> ptr1 = std::make_shared<int>(10);std::weak_ptr<int> ptr2 = ptr1;if(!ptr2.expired()) {std::cout << *ptr2.lock() << std::endl; // 输出: 10}return 0;
}
二.深浅拷贝
在C++中,**深拷贝(Deep Copy)和浅拷贝(Shallow Copy)**是两种不同的对象复制方式,主要区别在于如何处理对象中包含的指针或动态分配的资源。
1. 浅拷贝(Shallow Copy)
定义:浅拷贝是指简单地复制对象的所有成员变量,包括指针,但不复制指针指向的内存。这意味着两个对象将共享同一块动态内存。
特点:
快速:因为只复制指针,不需要分配新的内存。
共享资源:两个对象指向同一块内存,修改一个对象会影响另一个对象。
潜在问题:如果一个对象被销毁,另一个对象将持有悬空指针,可能导致未定义行为。
示例代码:
#include <iostream>
#include <string>class Person {
public:Person(const std::string& name_, int age_) : name(name_), age(age_) {std::cout << "Parameterized constructor called for " << name_ << std::endl;}// 默认拷贝构造函数(浅拷贝)Person(const Person& other) : name(other.name), age(other.age) {std::cout << "Copy constructor called for " << other.name << std::endl;}void display() const {std::cout << "Name: " << name << ", Age: " << age << std::endl;}private:std::string name;int age;
};int main() {Person p1("Alice", 30);Person p2 = p1; // 浅拷贝p2.display();return 0;
}输出:
Parameterized constructor called for Alice
Copy constructor called for Alice
Name: Alice, Age: 30
解释:
p2 是 p1 的浅拷贝,两个对象的 name 和 age 成员变量被复制。
如果 name 是 std::string,默认的拷贝构造函数会执行深拷贝,因为 std::string 内部已经处理了资源的复制。
2. 深拷贝(Deep Copy)
深拷贝是指不仅复制对象的所有成员变量,还复制指针指向的动态内存。这样,两个对象各自拥有独立的内存空间,互不影响。
特点:
独立资源:每个对象拥有自己独立的内存空间,修改一个对象不会影响另一个对象。
安全性:避免了悬空指针和内存冲突的问题。
开销较大:因为需要分配新的内存和复制数据。
示例代码:
#include <iostream>
#include <string>
#include <cstring>class Person {
public:Person(const std::string& name_, int age_) : name(new char[name_.size() + 1]), age(age_) {std::strcpy(name, name_.c_str());std::cout << "Parameterized constructor called for " << name_ << std::endl;}// 深拷贝构造函数Person(const Person& other) : name(new char[std::strlen(other.name) + 1]), age(other.age) {std::strcpy(name, other.name);std::cout << "Deep copy constructor called for " << name << std::endl;}// 赋值运算符(深拷贝)Person& operator=(const Person& other) {if (this != &other) {delete[] name;name = new char[std::strlen(other.name) + 1];std::strcpy(name, other.name);age = other.age;}std::cout << "Assignment operator called for " << name << std::endl;return *this;}~Person() {delete[] name;std::cout << "Destructor called for " << name << std::endl;}void display() const {std::cout << "Name: " << name << ", Age: " << age << std::endl;}private:char* name;int age;
};int main() {Person p1("Alice", 30);Person p2 = p1; // 深拷贝p2.display();return 0;
}输出:
Parameterized constructor called for Alice
Deep copy constructor called for Alice
Name: Alice, Age: 30
Destructor called for Alice
Destructor called for Alice
3. 何时使用深拷贝和浅拷贝
浅拷贝:
当对象中包含的指针指向的资源是共享的,或者不需要独立的副本时。
例如,std::string 和 std::vector 等标准库容器已经实现了深拷贝,因此使用默认拷贝构造函数即可。
深拷贝:
当对象中包含的指针指向的资源需要在多个对象之间独立存在时。
例如,类中包含指向动态分配内存的指针,需要在拷贝构造函数和赋值运算符中进行深拷贝,以避免内存泄漏和悬空指针。
解决方案
为了避免浅拷贝带来的问题,应该:
显式定义复制构造函数和赋值运算符:实现深拷贝,确保每个对象有独立的资源副本。
使用智能指针:如std::unique_ptr和std::shared_ptr,自动管理资源,避免手动管理内存的复杂性。
禁用复制构造函数和赋值运算符:如果不需要复制对象,可以通过= delete禁用复制构造函数和赋值运算符,防止浅拷贝。
class MyClass {
public:MyClass(const MyClass& other) = delete;MyClass& operator=(const MyClass& other) = delete;
};
三.构造函数使用new
在C++中,构造函数中使用new通常用于动态分配资源,如动态数组、指针指向的对象等。这种做法允许对象在创建时根据需要分配内存,从而实现更灵活的资源管理。然而,使用new也带来了责任,因为程序员必须确保在适当的时候释放这些动态分配的内存,以避免内存泄漏。
在构造函数中使用new主要出于以下目的:
动态分配内存:根据需要分配内存,而不是在编译时分配固定大小的内存。
初始化指针成员:为类的指针成员分配内存,以便存储动态数据。
管理资源:管理需要动态分配和释放的资源,如文件句柄、网络连接等。
实现方式
a. 分配单个对象
class MyClass {
private:int* data;
public:// 构造函数MyClass(int value) {data = new int(value);}// 析构函数~MyClass() {delete data;}// 成员函数void display() const {std::cout << *data << std::endl;}
};
b. 分配数组
class MyClass {
private:int* arr;int size;
public:// 构造函数MyClass(int s) : size(s) {arr = new int[size];for(int i = 0; i < size; ++i) {arr[i] = i;}}// 析构函数~MyClass() {delete[] arr;}// 成员函数void display() const {for(int i = 0; i < size; ++i) {std::cout << arr[i] << " ";}std::cout << std::endl;}
};
c. 使用初始化列表
为了提高效率,通常建议在初始化列表中初始化指针成员,而不是在构造函数体内赋值。
class MyClass {
private:int* data;
public:// 构造函数MyClass(int value) : data(new int(value)) {}// 析构函数~MyClass() {delete data;}// 成员函数void display() const {std::cout << *data << std::endl;}
};
注意事项
a. 资源管理
匹配分配和释放:每个new必须对应一个delete,每个new[]必须对应一个delete[]。
避免内存泄漏:确保在对象生命周期结束时正确释放动态分配的内存。
b. 异常安全
如果在构造函数中分配资源时发生异常,可能会导致资源泄漏。为了保证异常安全,可以使用RAII(资源获取即初始化)惯用法,或者使用智能指针。
#include <memory>class MyClass {
private:std::unique_ptr<int> data;
public:// 构造函数MyClass(int value) : data(std::make_unique<int>(value)) {}// 析构函数自动调用,不需要手动 delete~MyClass() = default;// 成员函数void display() const {std::cout << *data << std::endl;}
};
c. 拷贝构造函数和赋值运算符
如果类中包含指针成员,并且这些指针指向动态分配的资源,必须显式定义复制构造函数和赋值运算符,以实现深拷贝,避免多个对象共享同一份资源。
class MyClass {
private:int* data;
public:// 构造函数MyClass(int value) : data(new int(value)) {}// 复制构造函数(深拷贝)MyClass(const MyClass& other) : data(new int(*other.data)) {}// 赋值运算符重载(深拷贝)MyClass& operator=(const MyClass& other) {if(this == &other)return *this;delete data;data = new int(*other.data);return *this;}// 析构函数~MyClass() {delete data;}// 成员函数void display() const {std::cout << *data << std::endl;}
};
现代C++中的替代方案
为了简化资源管理,推荐使用智能指针(如std::unique_ptr和std::shared_ptr)来管理动态分配的资源。这些智能指针自动管理内存,减少了内存泄漏和悬挂指针的风险。
四.返回对象
在C++中,返回对象是指成员函数或自由函数返回一个对象(类的实例)。返回对象是C++中常见的操作,尤其在需要创建、复制或传递对象时非常有用。
返回对象是指函数通过return语句返回一个类的实例。这个实例可以是函数内部创建的对象,也可以是函数参数或成员变量的副本。
ClassName functionName(parameters) {// 创建或获取对象ClassName obj(parameters);return obj;
}
ClassName:返回的对象类型。
functionName:函数名称。
parameters:函数参数。
实现方式
a. 返回局部对象
函数可以返回在函数内部创建的对象。由于C++的返回值优化(Return Value Optimization,RVO)和命名返回值优化(Named Return Value Optimization,NRVO),这种做法通常是高效的
示例:
#include <iostream>
#include <string>class Person {
private:std::string name;int age;
public:Person(const std::string& name_, int age_) : name(name_), age(age_) {}Person getOlder() const {Person older(name, age + 1);//函数内部对象return older;}void display() const {std::cout << "Name: " << name << ", Age: " << age << std::endl;}
};int main() {Person alice("Alice", 30);Person olderAlice = alice.getOlder();olderAlice.display(); // 输出: Name: Alice, Age: 31return 0;
}
b. 返回成员对象
函数可以返回类的成员对象。这通常通过引用或指针实现,以避免不必要的拷贝。
示例:
#include <iostream>
#include <string>class Address {
public:std::string city;Address(const std::string& city_) : city(city_) {}
};class Person {
private:std::string name;Address address;//成员对象
public:Person(const std::string& name_, const std::string& city_) : name(name_), address(city_) {}Address getAddress() const {return address;}void display() const {std::cout << "Name: " << name << ", City: " << address.city << std::endl;}
};int main() {Person alice("Alice", "New York");Address aliceAddress = alice.getAddress();std::cout << aliceAddress.city << std::endl; // 输出: New Yorkreturn 0;
}
c. 返回指针或引用
为了避免拷贝开销,函数可以返回对象的指针或引用。然而,这需要确保返回的对象在函数返回后仍然有效,以避免悬挂指针或引用。
示例:
#include <iostream>
#include <string>class Person {
private:std::string name;int age;
public:Person(const std::string& name_, int age_) : name(name_), age(age_) {}const Person& getOlder() const {// 不推荐:返回对局部对象的引用Person older(name, age + 1);return older; // 未定义行为}void display() const {std::cout << "Name: " << name << ", Age: " << age << std::endl;}
};int main() {Person alice("Alice", 30);const Person& olderAlice = alice.getOlder(); // 未定义行为olderAlice.display();return 0;
}
五.返回const对象的引用
返回const对象的引用是指成员函数返回一个对const对象的引用,而不是返回对象的副本或非const引用。这意味着调用者可以访问对象,但不能修改它。
语法
const ClassName& functionName(parameters) const {// 实现代码return object;
}
const:修饰函数,表示函数不会修改对象的状态。
ClassName&:返回类型为ClassName的引用。
const ClassName&:返回类型为const ClassName的引用
用途
a. 单例模式
在单例模式中,通常提供一个静态成员函数来返回类的唯一实例。为了防止外部代码修改该实例,可以返回const引用。
示例:
#include <iostream>class Singleton {
private:static Singleton* instance;Singleton() {}// 禁止拷贝和赋值Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;
public:static const Singleton& getInstance() {if(!instance)instance = new Singleton();return *instance;}void doSomething() const {std::cout << "Singleton is doing something" << std::endl;}
};Singleton* Singleton::instance = nullptr;int main() {const Singleton& s = Singleton::getInstance();s.doSomething();// s = Singleton(); // 错误:不能赋值给 const 对象return 0;
}
b. 只读访问
当需要提供对内部数据的只读访问时,可以返回const引用,避免外部代码修改数据。
示例:
#include <iostream>
#include <string>class Person {
private:std::string name;int age;
public:Person(const std::string& name_, int age_) : name(name_), age(age_) {}const std::string& getName() const {return name;}int getAge() const {return age;}
};int main() {Person alice("Alice", 30);const std::string& nameRef = alice.getName();std::cout << nameRef << std::endl; // 输出: Alice// nameRef = "Alicia"; // 错误:不能修改 const 引用return 0;
}
实现方式
a.成员函数返回const引用
在类的成员函数中,可以返回const引用,以防止外部代码修改对象。
示例:
class MyClass {
private:int value;
public:MyClass(int val) : value(val) {}const int& getValue() const {return value;}
};
b. 全局或静态对象的const引用
有时,可以返回全局或静态对象的const引用,以提供只读访问。
示例:
#include <iostream>class Config {
public:int setting;// 其他配置参数
};const Config& getConfig() {static Config config;return config;
}int main() {const Config& config = getConfig();std::cout << config.setting << std::endl;// config.setting = 10; // 错误:不能修改 const 对象return 0;
}
注意事项
a. 避免返回局部对象的引用
返回const引用时,应确保引用的对象在函数返回后仍然有效。避免返回指向局部对象的引用,因为局部对象在函数返回后会被销毁,导致悬挂引用。
const int& getValue() {int value = 10;return value; // 错误:返回局部对象的引用
}
b. 常量正确性
确保返回const引用的对象在逻辑上是不可修改的。如果对象需要被修改,应返回非const引用或对象的副本。
c. 性能考虑
返回引用避免了拷贝操作,提高了性能。但需要权衡引用的生命周期和安全性。
六.static与const
const和static在C++中扮演着不同的角色:
const:用于声明常量,确保数据在初始化后不被修改,提高代码的安全性和可读性。
static:用于改变变量的生命周期和作用域,控制变量的可见性和存在时间。
static关键字用于声明静态变量或函数,其生命周期和作用域与普通变量或函数不同。
static可以用于:
静态变量:在函数内部声明的静态变量,在函数调用之间保持其值。
静态成员变量:在类中声明的静态成员变量,属于类本身,而不是某个特定的对象。
静态成员函数:在类中声明的静态成员函数,属于类本身,不依赖于任何对象。
const示例
#include <iostream>int main() {const int MAX = 100;// MAX = 200; // 错误:不能修改常量std::cout << "MAX: " << MAX << std::endl;return 0;
}
静态变量示例
#include <iostream>void func() {static int count = 0;count++;std::cout << "Count: " << count << std::endl;
}int main() {for(int i = 0; i < 5; ++i) {func(); // 输出: Count: 1 到 Count: 5}return 0;
}
静态成员变量示例
#include <iostream>class MyClass {
public:static int count;MyClass() { count++; }
};int MyClass::count = 0;int main() {MyClass obj1;MyClass obj2;std::cout << "Count: " << MyClass::count << std::endl; // 输出: Count: 2return 0;
}