数据结构-串
代码以及输出结果
//
// Created by 徐昌真 on 2024/11/7.
//
#include <iostream>
#include <cstring>
using namespace std;
class String{
private:
char *str;
size_t length;
public:
String();
String(const char *S);
String(const String &s);
~String();
size_t getLength() const;
char operator[] (size_t index) const;
String &operator= (const String& s);
bool operator== (const String &s) const; //这两用在赋值的时候避免自己给自己赋值 这里还没用 懒得搞
bool operator!= (const String &s) const; //
String copy() const;
String operator+ (const String& s);
friend ostream& operator<<(ostream &out, const String &s);
};
String::String() { //默认构造函数
str = new char[1];
length = 0;
str[0] = '\0';
}
String::String(const char *s) { //带参构造函数 给定字符串副本 传入*s是为了避免再次拷贝 消耗资源 使用char 而不是 string 是因为string还需要进行一次面量转换 消耗内存
length = strlen(s); //这个长度不包括/0
str = new char[length + 1]; //加入/0的位置
strcpy(str, s); //dest 目标
}
String::String(const String &s){ //拷贝构造函数 使用& 既是因为简洁 也是c++引用的规范代码
length = s.length;
str = new char[length + 1];
strcpy(str, s.str);
}
String::~String() { //析构
delete[] str;
}
size_t String::getLength() const { //获取字符串长度
return length;
}
char String::operator[] (size_t index) const { //返回索引元素
return str[index];
}
String &String::operator=(const String &s) { //字符串赋值
if (this != &s){
delete[] str;
length = s.length;
str = new char[length + 1];
strcpy(str, s.str);
}
return *this;
}
bool String::operator==(const String &s) const { //比较字符串是否相等
return strcmp(str, s.str) == 0;
}
bool String::operator!=(const String &s) const{ //比较字符串是否相等
return strcmp(str, s.str) != 0;
}
String String::copy() const{ //字符串复制
String s = *this; //这里调用了一次拷贝构造函数
return s;
}
String String::operator+(const String &s) { //字符串拼接
String result;
result.length = length + s.length;
result.str = new char[result.length + 1];
strcpy(result.str, str);
strcat(result.str, s.str);
return result;
}
ostream& operator<<(ostream& out, const String &s){ //输出流
out << s.str;
return out;
}
int main() {
String s("12345d");
String s1("1314");
cout << s << endl;
cout << s + "520" << endl;
cout << s[5] << endl;
cout << (s != "12345d") << endl;
s = s + "1314";
cout << s << endl;
String x = s.copy();
cout << x << ' ' << s << endl;
x = s = s1;
cout << x << ' ' << s << ' ' << s1 << endl;
return 0;
}
输出
一些疑问以及对代码的理解:(初学 如有不对或不在 望指出)
一.为什么传入的是*s 而不是char s 或者 string s
回答:在 C++ 中,`const char *s`、`char s[]`、以及 `std::string s` 各自的使用场景不同,选择 `const char *s` 是为了灵活性、性能和内存管理的便捷。具体原因如下:
1. **节省内存和提高效率**
使用 `const char *s` 传递字符串的内存地址,而不是复制整个字符串内容,可以节省内存。传递 `char s[]` 或 `std::string s` 会涉及到数组或者对象的拷贝,导致额外的内存分配和性能开销,尤其是对于较大的字符串而言。`const char *s` 直接传递地址,不需要额外的内存分配。
2. **C 风格字符串的兼容性**
在 C++ 中,很多旧的库函数和系统接口仍然使用 C 风格的字符串(即 `char*` 或 `const char*`),`strlen` 和 `strcpy` 等函数也操作的是 C 风格字符串。`const char *s` 使得 `String` 类可以直接接受 C 风格字符串作为参数,提供兼容性。
3. **避免不必要的构造和析构**
如果使用 `std::string s` 作为参数,编译器会在调用 `String` 构造函数时创建一个临时的 `std::string` 对象,而在函数退出时会自动销毁它。这会带来额外的构造和析构开销,不适合频繁的字符串传递操作。而 `const char *s` 可以直接操作字符串内容,更高效。
4. **允许用户传递字符串字面量**
`const char *s` 允许直接传入字符串字面量,例如 `"Hello"`。而 `std::string` 则要求字符串字面量先被转换为 `std::string` 类型,这种转换对有些代码场景来说是不必要的负担。`const char*` 类型在使用字面量字符串时无需进行任何额外的构造步骤。
综上所述,使用 `const char *s` 提供了更大的灵活性,允许调用者使用字符串字面量、字符数组、以及指向字符的指针传递字符串内容,从而简化了构造函数的调用。
二.拷贝构造函数用&而不是*
回答:在拷贝构造函数中使用引用(`&`)而不是指针(`*`)有几个关键原因:
1. **避免不必要的间接访问**
使用引用可以直接访问对象的成员,而使用指针则需要通过解引用(`*`)来访问对象的成员。引用比指针使用起来更简洁,代码的可读性更高。
2. **保证语法的一致性**
引用语法与普通对象访问的语法相同,不需要额外的符号来解引用,使得拷贝构造函数的使用更方便、直观。例如,`s.length` 和 `s.str` 是直接访问,而如果用指针,则需要写成 `s->length` 和 `s->str`。
3. **确保传递的是有效对象**
拷贝构造函数的参数使用引用,而不是指针,这样可以确保传入的一定是一个有效的对象(而不是 `nullptr`),因为在 C++ 中引用不能为 `nullptr`。这避免了拷贝构造函数中可能出现的空指针问题。
4. **符合 C++ 的拷贝构造函数规范**
在 C++ 中,拷贝构造函数通常被定义为 `ClassName(const ClassName& other);`。这是标准的定义方式,可以让编译器自动识别拷贝构造函数的特性,而使用指针则可能让代码的设计不符合常规习惯,导致编译器优化和语义理解上的差异。
### 总结
在拷贝构造函数中使用引用 `&` 而不是指针 `*` 可以提升代码的简洁性、安全性,并符合 C++ 的标准设计。
以上的个人总结:指针用来传递地址 引用就是引用对象 需要这个对象已经存在 拷贝构造函数不用指针而是用引用 一方面是代码简洁 安全性高 另一方面 这是c++拷贝构造函数的代码规范
三:我这里String构造的对象是同时存在的吗
回答:是的,当你通过 `String s("12345d");` 构造对象时,生成的 `String` 对象 `s` 会一直存在,直到它的生命周期结束(比如超出作用域或被显式销毁)。在 `String s("12345d");` 的过程中,`s` 是一个独立存在的对象,而这个对象的 `str` 成员则是一个动态分配的字符数组,它存储了 `"12345d"` 这段字符串的内容副本。
让我们看看对象的构建过程和对象的生命周期:
1. **创建对象 `s`**
`String s("12345d");` 创建了一个 `String` 类的对象 `s`,调用了带参构造函数 `String::String(const char* s)`。在这个构造函数中,程序会动态分配一块内存用于存储字符串内容的副本,即 `str = new char[length + 1];`。
2. **存储内容的副本**
带参构造函数会将传入的 C 风格字符串 `"12345d"` 复制到对象的 `str` 成员中。这个 `str` 是对象 `s` 的一个成员变量,属于对象 `s` 的内部状态。
3. **生命周期**
一旦 `String s("12345d");` 的作用域结束,例如在函数中 `s` 离开作用域,`s` 对象的析构函数(如果定义了)会被调用,以确保释放 `str` 动态分配的内存,防止内存泄漏。如果没有定义析构函数,可能导致内存泄漏问题。
4. **多个对象的情况**
如果创建多个 `String` 对象,比如 `String s1("hello");` 和 `String s2("world");`,它们是各自独立的对象。每个对象的 `str` 成员变量也会拥有自己的独立内存空间,不会相互干扰。这种独立性在构造函数中通过动态分配内存实现,每个对象的 `str` 都指向不同的内存地址。
因此,当你构造 `String` 对象时,这些对象确实是同时存在的,直到生命周期结束或者超出作用域为止。
四:可以再仔细说一下拷贝构造函数和copy的区别吗
回答:
当然可以!拷贝构造函数
和copy()
函数的主要区别在于它们的使用目的、调用时机和实现方式。我们可以逐项来详细比较。
1. 定义和目的
-
拷贝构造函数
拷贝构造函数的目的是在新对象初始化时,创建一个现有对象的副本。这是 C++ 的一种机制,用于在创建新对象时复制另一个对象的内容,确保新对象获得与原对象一样的内容和状态,但存储在独立的内存中。 -
copy() 函数
copy()
函数是一个普通成员函数,用于显式复制当前对象并返回一个副本。它提供了一个方法,可以在任何时候手动调用来获取当前对象的副本,而不仅限于对象初始化阶段。
2. 使用方式和调用时机
-
拷贝构造函数
- 在创建新对象并用另一个对象进行初始化时,拷贝构造函数会自动调用。例如:
String s1("Hello"); // 调用带参构造函数 String s2 = s1; // 调用拷贝构造函数,创建 s2 的副本 String s3(s1); // 也是调用拷贝构造函数,创建 s3 的副本
在函数传参时,如果参数按值传递,会调用拷贝构造函数。例如
-
void func(String str); // 按值传递,调用拷贝构造函数 func(s1); // s1 被拷贝传递给 func
copy()
函数是一个普通成员函数,只有在显式调用时才会执行。例如: -
String s1("Hello"); String s2 = s1.copy(); // 显式调用 copy() 函数,创建 s2 的副本
copy()
的调用不会由编译器自动触发,只有手动调用才会执行。-
3. 实现方式
-
拷贝构造函数
拷贝构造函数通过引用const String &s
接收一个现有对象,并为新对象分配内存,将内容复制过去。例如:String::String(const String &s) { length = s.length; str = new char[length + 1]; strcpy(str, s.str); }
-
这里会动态分配新的内存,确保新对象的
str
指向的是一块独立的内存区域。这样,两个对象在生命周期中互不影响。 -
copy() 函数
copy()
函数一般使用拷贝构造函数来创建副本。它的实现通常如下: -
String String::copy() const { return *this; // 直接返回当前对象的副本 }
-
这里的
return *this;
语句会调用拷贝构造函数,将*this
的内容复制给返回值。编译器可能会进行优化,以避免不必要的临时对象。 -
4. 内存管理与资源独立性
无论是拷贝构造函数还是
copy()
函数,都保证返回的副本对象与原对象拥有独立的内存。这意味着无论通过哪种方式创建副本,副本对象的str
指针都指向不同的内存地址,修改副本不会影响原对象,反之亦然。5. 具体区别总结
特性 拷贝构造函数 copy() 函数 调用方式 自动调用(创建新对象并用现有对象初始化时) 手动调用 主要用途 初始化时生成对象的副本 任何时候需要对象副本时可以调用 实现方式 接收对象引用,分配新内存并复制内容 内部调用拷贝构造函数 内存独立性 是 是 修改是否互相影响 否 否 总结
- 拷贝构造函数:用于对象初始化时自动复制现有对象。
- copy() 函数:是一个手动调用的成员函数,随时可以调用以获得当前对象的副本。
-
因此,尽管二者都可以用于生成对象的副本,但它们在使用方式和调用时机上是不同的。
-
个人理解:拷贝构造函数主要用于隐式调用 以及要初始化一个对象 copy只要用于创建副本 这个也可以说是代码规范性 为后续的开发维护提高代码的可读性
五:输出流的理解
-
ostream& operator<<(ostream& out, const String &s){ //输出流 out << s.str; return out; }
个人理解:ostream是一个输出流 << 运算符的重载返回值必须为ostream的对象 这里将s.str作为输出 out作为输出流 然后作为返回值输出
回答:是的,你的理解是对的。让我们再仔细理清楚这个过程。
1.
ostream
是输出流对象ostream
是 C++ 标准库中的输出流类,表示可以用来输出数据的流。常见的输出流对象有std::cout
(标准输出流)和文件流对象(如ofstream
)。ostream
类型提供了多个重载的<<
操作符,用于将不同类型的数据输出到流中。2.
<<
运算符重载的目标当我们重载
<<
运算符时,目标是让String
类型的对象能通过流插入操作符<<
被输出,就像输出int
、double
等内置类型一样。 out
是一个ostream&
类型的引用,它代表输出流(如std::cout
)。s.str
是String
对象s
内部的字符数组(C 风格字符串),我们希望将它输出到out
流中。-
3. 返回
ostream&
的原因<<
运算符重载的返回类型必须是ostream&
,这样做的目的是为了支持 链式调用。你可以使用多个<<
操作符在一行代码中输出多个内容,例如:cout << "Hello, " << "world!" << endl;
每个
<<
都会返回一个ostream&
,这样就可以继续进行下一个输出操作。如果返回的是void
,就不能链式调用了。4. 具体的实现
来看一下具体的实现:
ostream& operator<<(ostream& out, const String &s) { out << s.str; // 输出 String 对象中的字符数组 s.str return out; // 返回输出流对象 out,以便支持链式调用 }
out << s.str;
:这是输出操作,将s.str
(即String
对象内部的字符数组)输出到流out
(比如std::cout
)中。return out;
:返回流对象out
,使得我们可以继续执行后续的输出操作,比如:-
cout << "Hello" << myString << "World!";
- 这样就能顺畅地将多个输出操作连接在一起。
-
5. 理解流操作符的过程
out << s.str;
:将String
对象的内容(即s.str
)写入输出流out
。return out;
:返回流对象out
,使得我们可以继续在同一个流上执行其他输出操作。-
6. 总结
ostream& out
:是输出流的引用,表示你要将内容输出到哪个流(通常是std::cout
)。s.str
:是String
对象s
中存储的字符数组,实际存储了字符串的内容。- 返回
out
:为了支持链式调用,必须返回输出流对象out
,这样你就可以继续使用多个<<
运算符输出多个内容。 -
关于&
-
当你没有使用引用(即返回一个新的
ostream
对象)时,返回的流对象是一个新的副本。这意味着每次执行operator<<
时都会生成一个新的ostream
对象,这不仅浪费了内存和资源,还增加了不必要的复制操作。 - 这是因为每次调用
operator<<
后,out
流对象会被返回一个新的副本(而不是引用)。因此,当你尝试在返回值上继续进行流操作时,已经丢失了原始的流对象(比如std::cout
),这会导致无法继续执行后续的操作。 -
也就是鱼和熊掌不可兼得
六:String &String::operator=(const String &s) 这个代码的目的
String &String::operator=(const String &s) { //字符串赋值 if (this != &s){ delete[] str; length = s.length; str = new char[length + 1]; strcpy(str, s.str); } return *this; }
用于已有对象的赋值 而拷贝构造函数和copy都是用于初始化一个新的对象 这里&也是为了避免对象的再次拷贝 以及支持链式= = ; &是String的引用 而不是对象的引用
-
七:为什么+的重载不需要&
也就是说 需要长久存在的对象需要& 而临时存在的不需要对吗回答:
是的,你的理解基本正确。让我们总结一下:
1. 长久存在的对象需要返回引用 (
&
) - 赋值运算符 (
operator=
):你通常需要返回一个引用(String&
)来修改一个已经存在的对象。这允许链式赋值操作(例如a = b = c;
),并避免不必要的对象拷贝。 - 通过返回引用,你可以保证对已经存在的对象进行操作时不会发生对象复制,且允许对对象进行连续赋值。
&
的作用:引用确保赋值时不创建副本,返回的是原始对象的引用,这样才能链式调用并直接修改原对象。-
2. 临时对象不需要返回引用
- 加法运算符 (
operator+
) 和其他类似的运算符:你通常返回的是一个 新的临时对象,而不是修改现有的对象。返回 副本(而不是引用)是可以的,因为你只关心返回一个新的对象,并且这个新对象的生命周期仅限于函数调用结束后,调用者会接收它并可能将其赋值给某个变量。 - 返回值副本是合理的,因为你并不打算修改原对象,且返回的临时对象会在之后被销毁。
String operator+
:此运算符创建并返回一个新的String
对象,代表拼接后的字符串。返回一个副本是完全合适的。-
总结
- 需要修改现有对象的场景(如赋值运算符
operator=
),你需要返回引用String&
,因为这样可以避免多余的对象复制,并支持链式调用。 - 返回新的对象的场景(如加法运算符
operator+
),你返回的是一个新的临时对象副本,这个副本在函数返回后会被销毁,所以不需要返回引用。 -
这就是为什么像
operator+
这样的函数通常返回值(副本),而operator=
则返回引用(&
) -
但是我试了一下:没了&也可以正常输出和赋值 但是会经过多次拷贝 消耗资源 而且不符合代码规范
- 在创建新对象并用另一个对象进行初始化时,拷贝构造函数会自动调用。例如: