初次实现一个类
六个编译器为你生成的函数
手写一个类,我们需要知道创建类后,如果你没有显式定义构造/析构/拷贝/移动等函数,C++编译器会隐式地为你生成以下六个默认成员函数:
(1)默认构造函数--->用来初始化类对象,创建对象时会自动调用,用于初始化成员变量。如果你没有定义任何构造函数,编译器会自动生成这个函数。
ClassName();//没有参数
(2)默认拷贝构造函数--->使用现有对象创建一个新对象时调用。默认行为是按成员变量逐个复制。需要给这个函数传入const属性修饰的类对象引用。
ClassName(const ClassName& other);//唯一参数
(3)默认拷贝赋值运算符--->将一个已有对象赋值给另一个对象的时候调用,默认行为也是逐个成员变量复制,并返回 *this。用于实现深拷贝。
ClassName& operator=(const ClassName& other);
(4)默认移动构造函数--->将一个临时对象或即将被销毁的对象的资源“搬走”到新对象中,避免深拷贝开销。
ClassName(ClassName&& other);
(5)默认移动赋值运算符--->将一个右值对象的资源“搬”到另一个已有对象中。类似拷贝赋值,但资源转移而非复制。
ClassName& operator=(ClassName&& other);
(6)默认析构函数--->对象销毁时自动调用,用于释放资源。默认析构函数是空实现,但如果你的类中包含了指针或资源管理,就需要自定义析构函数。
~ClassName();//没有参数
通过 = default 和 = delete 控制是否使用这些默认函数。
✅ 显式定义(函数 = default)
告诉编译器:“这个函数我不自己写,仍然用你默认生成的就好”
class MyClass {
public:
MyClass() = default; // 默认构造函数
MyClass(const MyClass&) = default; // 拷贝构造
MyClass& operator=(const MyClass&) = default; // 拷贝赋值
MyClass(MyClass&&) = default; // 移动构造
MyClass& operator=(MyClass&&) = default; // 移动赋值
~MyClass() = default; // 析构函数
};
❌ 禁用默认函数(函数 = delete)
告诉编译器:“这个函数我不允许使用,谁调用谁编译错误”(用于单例模式、或者资源句柄)
class MyClass {
public:
MyClass() = default;
MyClass(const MyClass&) = delete; // 禁止拷贝构造
MyClass& operator=(const MyClass&) = delete; // 禁止拷贝赋值
MyClass(MyClass&&) = delete; // 禁止移动构造
MyClass& operator=(MyClass&&) = delete; // 禁止移动赋值
};
综合上面的描述,实现一个创建类的简单框架例子
class A{
//构造函数
A();
//拷贝构造函数
A(const A& other)
//拷贝赋值运算符
A& operator=(const A& other);
//移动构造函数
A(A&& other);
//移动赋值运算符
A& operator=(A&& other);
//析构函数
~A();
};
可以看出来规律:
拷贝相关的参数使用的是const 和 A&;移动相关的参数使用的是A&&。
构造相关的名字都是A,没有返回值;赋值运算符相关是返回值类型是A&另加一个operator运算符。
字符串底层结构
接下来进入今天讨论的主题:我们自己简单实现一个C++的字符串类String。
首先我们需要一个底层的数据结构,然后我们对这个数据结构进行操作管理,很容易想到C语言中的字符这个结构char。没错!要实现C++的String类来练习底层控制,可以使用封装C风格的字符串的方式,对C风格的字符串char*做一个管理来实现C++的字符串Sring。
C语言中char和C++中的String有什么本质区别呢?
(1)C++的String类是C++ STL的一个模板类,本质上是一个基于动态数组管理字符序列的容器。底层组成如下:
template<
class CharT,
class Traits = std::char_traits<CharT>,//字符特性(比较/复制等)操作类
class Allocator = std::allocator<CharT>//内存分配器
> class basic_string;
内部一般用 capacity() 来记录分配空间,size() 是当前字符数量。扩容方式:当字符数超出容量时,自动扩容(可能为 1.5x 或 2x);所有操作(比如拼接)都是自动管理内存,用户无需 new/delete;
(2)C风格字符串其实就是一段以 '\0'(空字符)结尾的连续char数组,不属于任何类或对象,靠一系列标准库函数(如strcpy, strlen)操作,以下总结了常用库函数:
| 函数名 | 作用 |
|---|---|
strlen(s) |
返回字符串长度(不含 \0) |
strcpy(dst, src) |
拷贝字符串 |
strcmp(a, b) |
字符串比较 |
strcat(dst, src) |
拼接字符串 |
strchr(s, ch) |
查找字符 |
由于String类实现起来比较复杂,我们今天就只要实现一个轻量化的字符串类,了解一下底层原理即可。
String类需要哪些功能?
使用的数据结构
C风格字符串类型包括char或者char[],考虑到char[]是固定大小的静态数组不能在运行的时候动态调整大小,所以我们可以采用char,它的本质就是一个字符指针,这种形式可以由程序员进行手动管理,使用new char[n]来分配,管理起来更加灵活。
char* c;
构造函数
使用一个有参的构造函数,构造一个字符串,需要分配空间,并且初始化。接收一个char*类型的字符串,默认初始化为0,也就是如果构造函数没有传入参数,默认就是nullptr。
🔍核心逻辑:判断,如果传入非空字符串,需要为其分配内存空间,否则就只需要为'\0'分配一个字符空间。
String(const char* cstr = 0){
if(cstr){
c = new char[strlrn(cstr)+1]; //动态分配一块堆内存,存储字符串内容;返回指针赋值给成员变量 c;
c = strcpy(c, cstr); //传入的字符串cstr拷贝到刚才分配的内存c中
} else {
c = new char[1];
char[0] = '\0';
}
}
拷贝构造函数
传入唯一参数就是String类型的对象引用,从拷贝构造函数的功能来看,也就是通过其他的字符串来创建新的字符串。
🔍核心逻辑:可以先分配这个传入字符串大小的内存空间(other.c就表示其他通过String类创建出来的字符串对象),然后将它的字符串复制到新的字符串。
String(const String& other){
c = new char[strlen(other.c) + 1];
c = strcpy(s, other.c);
}
拷贝赋值运算符
它的作用是在两个String对象之间执行赋值操作时可以正确地管理内存,避免内存泄漏和浅拷贝错误。
❗注意拷贝赋值运算符和拷贝构造函数是不一样的。因为拷贝构造函数只有一个已有的对象,需要创建新的对象,分配一下内存然后把原有对象的内容复制过来。而拷贝赋值运算符是已经有了两个对象,现在要把一个对象复制到另一个对象。
🔍 核心逻辑:如果两个对象不一样,可以先释放原先分配的空间,然后再和拷贝构造函数一样分配空间复制一下,那如果这两个对象一模一样比如a = a,这个时候直接返回不会出错,【进一步思考】如果不加一步判断可以吗?答案是不行。这样做可能会出现将自己的字符串删掉了,释放了自己的内存后赋值给自己,此时复制的是空的。最后拷贝赋值运算符需要返回值通常是自身的引用,所以我们返回一个this指针。(返回自身的引用可以防止多)
String& operator=(const String& other){
if(this != &other){ //this是指向当前对象的指针,&other是赋值给当前对象的另一个对象的地址
delete[] c;
c = new char[strlen(other.c) + 1];
c = strcpy(c, other.c);
}
return *this; //返回 *this,使得支持 a = b = c链式赋值
}
接下来我们来看一下,C++中一个核心概念:指针、引用、对象本身*this。
①首先需要知道类中的非静态成员函数它们都有一个隐藏的this指针,这个指针会指向具体对象,帮助我们识别这个方法是由哪个对象调用的。这也是实现C++多态的关键,正是因为有了this指针,我们在调用同名函数的时候,编译器才知道具体调用的哪个实现。这个this的类型是ClassName,在这里就是String。
而this是对this这个指针的解引用,就表示‘当前对象本身’,this的类型是“当前对象本身”,是String&,也就是当前对象的引用。不过return *this可以返回String&,也可以返回String。
String c = "hello";
String* cptr = &c; //&c表示取c这个变量的地址
//以上就表示 *cptr == c == *(&c)
| 符号 | 含义 | 举例 |
|---|---|---|
&x |
取地址,得到指针(地址值) | int* p = &x; |
*p |
解引用,得到指针所指的值 | int val = *p; |
②我们可以看到上面表达式返回的是String&也就是字符串引用类型,而不是String类型。为什么指针解引用 *ptr 得到的是 T&(引用类型)而不是 T(值类型)?因为解引用某个指针,是指访问指针所指向的变量,直接操作原值,而不是复制一份它的值。
而如果返回的是String类型,也就是和变量同类型本质上是重新创建了一个变量,并且将原来变量的值复制过来。这就有了两个变量,这样你操作的时候不会操作到原来的变量。语法上可以,不过这里不要这样用。
| 返回类型 | 含义 | 后果 |
|---|---|---|
String& |
返回当前对象的引用 | ✅ 支持链式赋值,✅ 无额外拷贝 |
String |
返回当前对象的副本(拷贝) | ❌ 不能链式赋值,⚠️ 多一次拷贝开销 |
③再来说一下为什么返回String&可以实现链式赋值?
a = b = c; 是从右往左计算的,b = c 先执行完后,返回 b 的引用,再赋值给 a,这就要求赋值操作返回自身的引用。如果operator=返回的是引用,下一次赋值就可以继续使用返回的对象。可以理解为:“我已经赋值完了,现在把我自己(引用)交给下一个赋值语句继续用。”
如果返回值类型是String,本质上是进行了一步复制操作,将原来的值复制到新变量中,这不仅会浪费性能,并且由于这样做不能链接两个变量,也就是b = c(c的副本),a = b(b的副本),三者并不关联在一起。
移动构造函数
拷贝构造函数是深拷贝,性能较低;如果我们只需要“偷走”资源,移动构造就更高效。
🔍 核心逻辑:将其他对象的值先复制过来,然后将其他对象的指针置为空。⚠️不能释放其他对象的内存。移动语义的目标是“偷走资源”而不是“销毁资源”,不能销毁其他对象的变量;
String(String&& other) noexcept{
c = other.c;
other.c = nullptr;
}
noexcept关键字的作用是:“我保证这个函数调用过程中不会抛出异常。如果抛了异常,程序直接终止(std::terminate())。”能够提高性能、保障容器行为、减少异常风险。如果不加入这个关键字,编译器会担心抛出异常,从而选择安全的拷贝构造,降低性能。
移动赋值运算符
返回偷过来的资源给当前对象的变量。注意和移动构造不一样在于,移动构造是偷资源用来构造,原本就没有资源。而移动赋值运算符是已经有了资源,然后如果和别人不一样的话还要去偷资源
String& operator=(String&& other){
if(this != &other){
delete[] c;
c = other.c;
other.c = nullptr;
}
return *this;
}
析构函数
~String(){
delete[] c;
}
重载<<运算符函数
这个函数的作用是:自定义类如何实现 cout << 对象 这样的行为:
String s("hello");
std::cout << s << std::endl;
🔍 核心逻辑:传入的参数是一个输出流,ostream&类型的输出流os(类似cout),以及一个具体的要输出的对象,然后将具体对象的变量传给os。最后返回输出流本身,这种方式和前面的移动赋值运算符一样,支持链式调用:std::cout << s1 << s2 << std::endl;
friend ostream& operator<<(ostream& os, const String& s){
os << s.c;
return os;
}
由于运算符左边的操作数不是String类的对象,而是std::ostream类型的对象(比如std::cout),所以这个运算符重载函数不能算是String的成员函数(因为成员函数的隐式左操作数是this,必须是String类型);
使用friend关键字,就可以使这个友元函数可以访问String类的私有变量c。
重载 operator== 和 operator!=
🔍 核心逻辑:直接用strcmp比较,或者使用*this与other比较。
bool operator==(const String& other) const {
return strcmp(c, other.c) == 0; //返回值是0=字符串相等
}
bool operator!=(const String& other) const {
return !(*this == other); //完全相等==>假(也就是不是不相等的字符串)
}
利用关键字strcmp来比较两个字符串,如果strcmp(s1, s2)返回值为0表示两个字符串完全相等,如果小于0,就表示s1字典序小于s2。
添加 size() 方法
🔍 核心逻辑:直接返回strlen()
size_t size() const{
return strlen(c);
}
添加 operator[] 访问字符
char& operator[](size_t index){
return c[index];
}
const char& operator[](size_t index) const{
return c[index];
}
添加 append()、+ 运算符
String operator+(const String& other){
char* newC = new char[strlen(c) + strlen(other.c) + 1];
strcpy(newC, c);
strcat(newC, other.c); //以上都只是普通C字符串的操作
String result(newC); //利用普通的C字符串,构造出String类对象result
delete[] newC;
return result; //返回String类对象
}
字符串转为int类型
int StrToInt(bool* ok = nullptr) const{ //先传入一个bool类型的空指针
if(ok) *ok = false; //先要设置ok指针为false
if(!c || *c == '\0') return -1; //如果字符串为空指针或者空字符串-->返回-1
char* end = nullptr; //用来存储不能转换部分的字符串
int i = strtol(c, &end, 10); //字符串的起始位置c,&end转换结束,指向不能转换的位置
if(ok) *ok = (*end == '\0'); //如果end最后指向的不是'\0'说明有不是数字的字符在后面
return i;
}
strtol关键字的用法-->long strtol(const char* str, char** endptr, int base);可以看出来,需要传入end指针的地址才可以让strtol函数修改end的值。
假设 c = "123abc":
strtol 从 c 开始读取,发现能合法读取出 123
它会让 end 指向 'a' 的位置
c ---> '1' '2' '3' 'a' 'b' 'c' '\0'
↑
strtol 从这里开始
end ---> 'a' 'b' 'c' '\0' ← strtol 填充的值
代码示例
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
class String
{
private:
char* c;
//采用char*的形式,而不是char[],对C风格字符串的封装来实现string类
public:
String(const char* cstr = 0) {
if (cstr) {
c = new char[strlen(cstr) + 1];
strcpy(c, cstr);
} else {
c = new char[1];
c[0] = '\0';
}
}
~String() {
delete[] c;
}
String(const String& other) {
c = new char[strlen(other.c) + 1];
strcpy(c, other.c);
}
String& operator=(const String& other) {
if (this != &other) {
delete[] c;
c = new char[strlen(other.c) + 1];
strcpy(c, other.c);
}
return *this;
}
String(String&& other) noexcept {
c = other.c;
other.c = nullptr;
//不能释放其他对象的资源,目标是偷走,不是销毁对象的变量,delete[] other.c;
}
String& operator=(String &&other) noexcept {
if (this != &other)
{
delete[] c;
c = other.c;
other.c = nullptr;
}
return *this;
}
friend ostream& operator<<(ostream& os, const String& s) {
os << s.c;
return os;
}
bool operator==(const String &other) const {
return strcmp(c, other.c) == 0;
}
bool operator!=(const String &other) const {
return !(*this == other);
}
size_t size() const {
return strlen(c);
}
char &operator[](size_t index) {
return c[index];
}
const char &operator[](size_t index) const {
return c[index];
}
String operator+(const String &other) const {
char *newC = new char[strlen(c) + strlen(other.c) + 1];
strcpy(newC, c);
strcat(newC, other.c);
String result(newC);
delete[] newC;
return result;
}
int toInt(bool* ok = nullptr) const {
if (ok) *ok = false;
if (!c || *c == '\0') return 0;
char* end = nullptr;
int i = strtol(c, &end, 10);
if (ok) *ok = (*end == '\0');
return i;
}
};
int main() {
// 构造函数测试
String s1("Hello");
String s2("World");
cout << "s1: " << s1 << endl;
cout << "s2: " << s2 << endl;
// 拷贝构造函数测试
String s3 = s1;
cout << "【拷贝构造】用s1拷贝构造出s3: " << s3 << endl;
// 移动构造函数测试
String s4 = String("Temp");
cout << "【移动构造】用Temp移动构造出s4 : " << s4 << endl;
// 拷贝赋值测试
s3 = s2;
cout << "【拷贝赋值】s3拷贝赋值s2的数据: " << s3 << endl;
// 移动赋值测试
s4 = String("NewTemp");
cout << "【移动赋值】s4原本有数据,现在移动赋值NewTemp: " << s4 << endl;
// 比较运算符测试
String s5("Hello");
cout << "s1与s5是否相等:" << (s1 == s5) << endl; // true
cout << "s1与s2是否不相等:" << (s1 != s2) << endl; // true
// 索引运算符测试
cout << "s1[1]: " << s1[1] << endl; // 'e'
s1[1] = 'a';
cout << "修改s1的下标1元素后:" << s1 << endl; // Hallo
// 字符串拼接测试
String s6 = s1 + s2;
cout << "s6 = s1 + s2: " << s6 << endl; // HalloWorld
// size() 测试
cout << "s6 size: " << s6.size() << endl; // 10
// toInt() 测试
String s7("12345");
bool ok;
int val = s7.toInt(&ok);
cout << "s7 toInt: " << val << ", 是否转化成功:" << ok << endl; // 12345, true
String s8("123abc");
val = s8.toInt(&ok);
cout << "s8 toInt: " << val << ", 是否转化成功:" << ok << endl; // 123, false
String s9("abc");
val = s9.toInt(&ok);
cout << "s9 toInt: " << val << ", 是否转化成功:" << ok << endl; // 0, false
String s10("");
val = s10.toInt(&ok);
cout << "s10 toInt: " << val << ", 是否转化成功:" << ok << endl; // 0, false
return 0;
}
测试结果:

浙公网安备 33010602011771号