数据结构-串

代码以及输出结果

//
// 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` 直接传递地址,不需要额外的内存分配。

  1. **C 风格字符串的兼容性**

在 C++ 中,很多旧的库函数和系统接口仍然使用 C 风格的字符串(即 `char*` 或 `const char*`),`strlen` 和 `strcpy` 等函数也操作的是 C 风格字符串。`const char *s` 使得 `String` 类可以直接接受 C 风格字符串作为参数,提供兼容性。

  1. **避免不必要的构造和析构**

如果使用 `std::string s` 作为参数,编译器会在调用 `String` 构造函数时创建一个临时的 `std::string` 对象,而在函数退出时会自动销毁它。这会带来额外的构造和析构开销,不适合频繁的字符串传递操作。而 `const char *s` 可以直接操作字符串内容,更高效。

  1. **允许用户传递字符串字面量**

`const char *s` 允许直接传入字符串字面量,例如 `"Hello"`。而 `std::string` 则要求字符串字面量先被转换为 `std::string` 类型,这种转换对有些代码场景来说是不必要的负担。`const char*` 类型在使用字面量字符串时无需进行任何额外的构造步骤。

综上所述,使用 `const char *s` 提供了更大的灵活性,允许调用者使用字符串字面量、字符数组、以及指向字符的指针传递字符串内容,从而简化了构造函数的调用。

.拷贝构造函数用&而不是*

回答:在拷贝构造函数中使用引用(`&`)而不是指针(`*`)有几个关键原因:

  1. **避免不必要的间接访问**

使用引用可以直接访问对象的成员,而使用指针则需要通过解引用(`*`)来访问对象的成员。引用比指针使用起来更简洁,代码的可读性更高。

  1. **保证语法的一致性**

引用语法与普通对象访问的语法相同,不需要额外的符号来解引用,使得拷贝构造函数的使用更方便、直观。例如,`s.length` 和 `s.str` 是直接访问,而如果用指针,则需要写成 `s->length` 和 `s->str`。

  1. **确保传递的是有效对象**

拷贝构造函数的参数使用引用,而不是指针,这样可以确保传入的一定是一个有效的对象(而不是 `nullptr`),因为在 C++ 中引用不能为 `nullptr`。这避免了拷贝构造函数中可能出现的空指针问题。

  1. **符合 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];`。

  1. **存储内容的副本**

带参构造函数会将传入的 C 风格字符串 `"12345d"` 复制到对象的 `str` 成员中。这个 `str` 是对象 `s` 的一个成员变量,属于对象 `s` 的内部状态。

  1. **生命周期**

一旦 `String s("12345d");` 的作用域结束,例如在函数中 `s` 离开作用域,`s` 对象的析构函数(如果定义了)会被调用,以确保释放 `str` 动态分配的内存,防止内存泄漏。如果没有定义析构函数,可能导致内存泄漏问题。

  1. **多个对象的情况**

如果创建多个 `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 类型的对象能通过流插入操作符 << 被输出,就像输出 intdouble 等内置类型一样。

    • out 是一个 ostream& 类型的引用,它代表输出流(如 std::cout)。
    • s.strString 对象 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= 则返回引用(&

    但是我试了一下:没了&也可以正常输出和赋值 但是会经过多次拷贝 消耗资源 而且不符合代码规范

    我给你说一下我的理解 也就是返回值需要保留的需要& 返回值用完就不要了的 也就是临时的 就不需要&

    • 返回值需要长期保留的 (例如:赋值运算符 operator= 返回当前对象的引用以支持链式赋值)需要返回引用 &
    • 返回值是临时的、不需要保留的 (例如:operator+ 返回一个新对象作为拼接结果,调用完成后可以丢弃原始数据)则不需要引用。
    • 这样可以避免不必要的内存问题,同时保持代码简洁。你这样理解非常到位!
相关推荐
I_Am_Me_3 分钟前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
暮色_年华4 分钟前
Modern Effective C++item 9:优先考虑别名声明而非typedef
c++
重生之我是数学王子13 分钟前
QT基础 编码问题 定时器 事件 绘图事件 keyPressEvent QT5.12.3环境 C++实现
开发语言·c++·qt
Ai 编码助手14 分钟前
使用php和Xunsearch提升音乐网站的歌曲搜索效果
开发语言·php
学习前端的小z18 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
daiyang123...26 分钟前
测试岗位应该学什么
数据结构
神仙别闹26 分钟前
基于C#和Sql Server 2008实现的(WinForm)订单生成系统
开发语言·c#
XINGTECODE27 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
我们的五年36 分钟前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
kitesxian39 分钟前
Leetcode448. 找到所有数组中消失的数字(HOT100)+Leetcode139. 单词拆分(HOT100)
数据结构·算法·leetcode