当前位置: 首页 > article >正文

数据结构-串

代码以及输出结果

//
// 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 类型的对象能通过流插入操作符 << 被输出,就像输出 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= 则返回引用(&

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


http://www.kler.cn/a/388255.html

相关文章:

  • idea的mapper.xml文件里写sql语句出现Tag name expected错误提示
  • html+js+css实现拖拽式便签留言
  • 前端入门一之ES6--面向对象、够着函数和原型、继承、ES5新增方法、函数进阶、严格模式、高阶函数、闭包
  • LinkedList和单双链表。
  • 在Linux上部署(MySQL Redis Elasticsearch等)各类软件
  • 无人机反制技术与方法:主动防御,被动防御技术原理详解
  • 【论文笔记】Parameter-Efficient Transfer Learning for NLP
  • 软件设计师:排序算法总结
  • ReactPress数据库表结构设计全面分析
  • 前端学习之ES6+
  • 七大经典基于比较排序算法【Java实现】
  • Elasticsearch实战应用:打造高效的全文搜索与高亮显示功能
  • Python实现粒子滤波算法
  • 1024程序员节|借势AI,写出牛码
  • jmeter常用配置元件介绍总结之jsr223执行python脚本
  • 【温度表达转化】
  • mybatis-plus 长sql执行缓慢问题
  • 【前端】Svelte:核心语法和组件基础
  • RabbitMQ — 异步调用
  • 【Webpack配置全解析】打造你的专属构建流程️(4)
  • 【解决】Pico 串流 Unity 开发环境 Preview 黑屏问题
  • 现代化汽车共享:SpringBoot管理系统开发
  • 数据与Python
  • 企业内部知识库搭建工具精选
  • 用js去除变量里的html标签
  • Android gradle下载失败后 解决方案