Day 4:explicit、析构函数、动态内存和常用类
今天目标:继续学习类与对象后面的内容,理解 explicit、析构函数、new/delete、nullptr,并通过链表、string、目录读取代码练习对象封装。
对应代码:
2-代码/day4-/18-explicit示例.cpp
2-代码/day4-/19-析构函数示例.cpp
2-代码/day4-/20-动态内存示例.cpp
2-代码/day4-/test.cpp
2-代码/day4-/list.hpp
2-代码/day4-/list.cpp
2-代码/day4-/21-test-list.cpp
2-代码/day4-/22-string使用示例.cpp
2-代码/day4-/dir.hpp
2-代码/day4-/dir.cpp
2-代码/day4-/23-test-dir.cpp
1、explicit 关键字
explicit 通常用来修饰构造函数,作用是:禁止编译器通过这个构造函数进行隐式类型转换。
先看没有 explicit 的情况:
class Demo
{
public:
Demo(int x)
{
_x = x;
}
private:
int _x;
};
int main()
{
Demo d1(100); // 正常:直接调用构造函数
Demo d2 = 200; // 也能通过:编译器会自动转换成 Demo(200)
return 0;
}
这里的:
Demo d2 = 200;
表面上看是把 int 赋给 Demo,实际上编译器会偷偷帮你变成:
Demo d2 = Demo(200);
这就叫隐式类型转换。
如果构造函数前面加上 explicit:
class Demo
{
public:
explicit Demo(int x)
{
_x = x;
}
private:
int _x;
};
int main()
{
Demo d1(100); // 正确:明确调用构造函数
Demo d2{200}; // 正确:明确调用构造函数
// Demo d3 = 300; // 错误:explicit 禁止隐式转换
return 0;
}
一句话记忆:
explicit 用来防止“看起来不像创建对象,但实际被编译器偷偷转成对象”的写法。
常见建议:
如果一个构造函数只接收一个参数,通常建议加 explicit,避免意外转换。
2、析构函数
析构函数也是类中的特殊成员函数,作用是:对象销毁时自动调用,一般用来释放资源。
基本特点:
1. 函数名是 ~类名
2. 没有返回值
3. 不能写参数
4. 一个类只能有一个析构函数
5. 对象生命周期结束时自动调用
示例:
#include <iostream>
using namespace std;
class Demo
{
public:
Demo()
{
cout << "Demo()" << endl;
}
~Demo()
{
cout << "~Demo()" << endl;
}
};
int main()
{
Demo d1;
cout << "-------" << endl;
return 0;
}
执行过程:
1. 创建 d1,自动调用构造函数 Demo()
2. main 函数结束,d1 销毁,自动调用析构函数 ~Demo()
构造函数和析构函数可以这样理解:
构造函数:对象出生时自动调用
析构函数:对象死亡时自动调用
3、全局对象的构造和析构顺序
练习代码 test.cpp 中定义了一个全局对象:
#include <iostream>
using namespace std;
class Print
{
public:
Print()
{
cout << "hello" << endl;
}
~Print()
{
cout << "world" << endl;
}
};
Print p;
int main()
{
cout << "cpp" << endl;
return 0;
}
输出结果:
hello
cpp
world
原因:
1. 全局对象 p 在 main 函数执行之前创建,所以先输出 hello
2. main 函数执行,输出 cpp
3. 程序结束时,全局对象 p 被销毁,所以最后输出 world
这个练习说明:不修改 main 函数,也可以通过全局对象的构造和析构,在 main 前后执行代码。
4、动态内存 new 和 delete
C++ 中,对象的内存大致分为三类:
局部对象 栈区
全局对象 / 静态对象 静态存储区
动态对象 堆区
C 语言中常用:
malloc();
free();
C++ 中处理对象时更推荐:
new;
delete;
原因是:new/delete 不只是分配和释放内存,还会自动调用构造函数和析构函数。
4.1、创建单个对象
Demo* p1 = new Demo(100);
p1->print();
delete p1;
执行过程:
new Demo(100) 分配内存,并调用构造函数
p1->print() 通过指针调用成员函数
delete p1 调用析构函数,并释放内存
注意:
p1->print();
等价于:
(*p1).print();
-> 是通过对象指针访问成员时使用的运算符。
4.2、创建对象数组
Demo* p2 = new Demo[5];
delete []p2;
注意匹配关系:
new 单个对象 对应 delete
new [] 对象数组 对应 delete []
错误示例:
Demo* p = new Demo[5];
delete p; // 错误,应该使用 delete []
一句话记忆:
怎么 new,就怎么 delete;带 [] 的释放时也要带 []。
5、nullptr 空指针
传统 C/C++ 中经常用 NULL 表示空指针,但在 C++ 中,NULL 有时会造成函数重载匹配不清楚。
示例:
void foo(int x);
void foo(int* p);
// foo(NULL); // 可能产生二义性
foo(nullptr); // 明确表示空指针
nullptr 是 C++11 引入的空指针字面值,专门表示空指针。
对比:
NULL 本质上通常是 0,可能被当作整数
nullptr 专门表示空指针,类型更明确
建议:
C++ 中表示空指针,优先使用 nullptr。
6、双向链表 List
今天的 list.hpp 和 list.cpp 用类封装了一个简单的双向链表。
链表类的主要成员:
class List
{
public:
List();
~List();
void push_back(Type x);
void push_front(Type x);
void pop_back();
void pop_front();
Type back() const;
Type front() const;
int size() const;
bool empty() const;
void print() const;
private:
struct Node
{
Node(Type data);
Type _data;
Node* _next;
Node* _prev;
};
Node* _head;
Node* _tail;
int _size;
};
成员含义:
_head 指向第一个结点
_tail 指向最后一个结点
_size 记录链表中结点个数
_next 指向下一个结点
_prev 指向上一个结点
6.1、push_back 尾插
void List::push_back(Type x)
{
Node* p = new Node(x);
if (_size == 0)
{
_head = p;
_tail = p;
}
else
{
p->_prev = _tail;
_tail->_next = p;
_tail = p;
}
++_size;
}
逻辑:
1. 先 new 一个新结点
2. 如果链表为空,新结点同时是头和尾
3. 如果链表不为空,把新结点接到原来的尾结点后面
4. 更新 _tail
5. _size 加 1
6.2、push_front 头插
void List::push_front(Type x)
{
Node* p = new Node(x);
if (_size == 0)
{
_head = p;
_tail = p;
}
else
{
p->_next = _head;
_head->_prev = p;
_head = p;
}
++_size;
}
逻辑和尾插类似,只是插入位置变成链表头部。
6.3、pop_back 和 pop_front
删除结点时要注意三件事:
1. 空链表不能删除
2. 删除结点前,要先保存要删除的结点指针
3. delete 后,要修正 _head、_tail、_next、_prev
尾删:
void List::pop_back()
{
if (_size == 0)
{
return;
}
Node* p = _tail;
_tail = p->_prev;
delete p;
--_size;
if (_size == 0)
{
_head = nullptr;
_tail = nullptr;
}
else
{
_tail->_next = nullptr;
}
}
6.4、链表和析构函数
链表中的每个结点都是通过 new Node(x) 创建的,所以最终应该在析构函数里释放所有结点。
推荐写法:
List::~List()
{
while (!empty())
{
pop_front();
}
}
这样当 List 对象销毁时,链表中的所有动态结点都会被释放。
7、字符串类型 std::string
C++ 标准库提供了 std::string 表示字符串。
使用前包含头文件:
#include <string>
常见创建方式:
std::string s1; // 空字符串
std::string s2("hello"); // 使用 C 字符串初始化
std::string s3{"hello"}; // C++11 大括号初始化
std::string s4 = "hello"; // 直接赋值初始化
std::string s5(5, 'a'); // "aaaaa"
今天代码中的常用接口:
std::string s1 = "hello";
cout << s1.size() << endl; // 字符个数
cout << s1.empty() << endl; // 是否为空
const char* str = s1.c_str(); // 转成 const char*
size_t index = s1.find("oo"); // 查找子串
if (index == std::string::npos)
{
cout << "not found" << endl;
}
7.1、[] 和 at() 的区别
cout << s1[10] << endl; // 越界时结果未知
cout << s1.at(10) << endl; // 越界时会抛出异常
简单理解:
[] 速度快,但不检查越界
at() 会检查越界,更安全
7.2、find 和 npos
size_t index = s1.find("oo");
如果找到,返回子串第一次出现的位置;如果没找到,返回:
std::string::npos
判断方式:
if (index == std::string::npos)
{
cout << "not found" << endl;
}
8、目录操作 Dir 类
dir.hpp 和 dir.cpp 把 Linux 目录读取封装成了一个 Dir 类。
相关头文件:
#include <dirent.h>
核心函数:
opendir() 打开目录
readdir() 读取目录项
closedir() 关闭目录
类的声明:
class Dir
{
public:
Dir(std::string path);
~Dir();
List getAllFilenames();
private:
std::string _path;
DIR* _pdir;
};
8.1、构造函数打开目录
Dir::Dir(std::string path) : _path(path)
{
_pdir = opendir(_path.c_str());
if (_pdir == nullptr)
{
perror("opendir error");
exit(1);
}
}
说明:
_path.c_str() 把 std::string 转成 const char*
opendir() 打开成功会返回 DIR*,失败返回 nullptr
8.2、析构函数关闭目录
Dir::~Dir()
{
closedir(_pdir);
}
这个例子很好地体现了析构函数的作用:
构造函数负责打开资源
析构函数负责释放资源
8.3、读取所有文件名
List Dir::getAllFilenames()
{
List list;
while (1)
{
struct dirent* p = readdir(_pdir);
if (p == nullptr)
{
break;
}
std::string pathfile = _path + p->d_name;
list.push_back(pathfile);
}
return list;
}
使用方式:
Dir dir("./");
List list = dir.getAllFilenames();
list.print();
注意:
Dir 这部分代码依赖 Linux/POSIX 的 dirent.h,在 Windows 原生环境下可能不能直接编译。
9、Day4 总结
1. explicit 用来禁止构造函数参与隐式类型转换。
2. 析构函数写成 ~类名(),对象销毁时自动调用。
3. 构造函数适合申请或打开资源,析构函数适合释放或关闭资源。
4. new 创建动态对象,会调用构造函数。
5. delete 释放动态对象,会调用析构函数。
6. new[] 要配 delete[],new 要配 delete。
7. C++ 中空指针优先使用 nullptr,不推荐继续用 NULL。
8. 链表结点通常用 new 创建,删除时必须 delete。
9. std::string 比 C 字符串更方便,常用 size、empty、at、c_str、find。
10. opendir/readdir/closedir 可以读取 Linux 目录内容。
一句话记忆:
explicit 管转换,析构函数管收尾,new/delete 管堆内存,string 管字符串,Dir 类把目录操作封装成对象。






Comments | NOTHING