C++学习笔记之string类

本文详细介绍了C++中的string类,包括常用的构造函数、容量操作、访问和修改操作。重点讨论了拷贝构造函数、赋值运算符以及深浅拷贝的区别。此外,还提到了string类的模拟实现和LeetCode上的相关练习题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

c语言中,字符串是以'\0'结尾的一些字符的集合。

如果说需要用到C语言的一些字符串里面的函数,头文件是#include<cstring>。

#include<string>是C++里面的string。

将从以上这几个方面进行阐述说明。 

https://blog.youkuaiyun.com/u011000290/article/details/49020335

大佬把拷贝构造,构造,析构函数,赋值函数很清楚。大佬不愧是大佬。

string类的常用接口说明

1.string类对象的常见构造。 

#include<iostream>
#include<cstdio>
#include<string>
using namespace std;
int main()
{
  string s1;
  //string() 构造空的string类对象,即空字符串
  string s2("hello world!");
  //string(const char* s)用c-string来构造string类对象
  string s3(10,'a');
  //string(size_t n,char c)string类对象包含n个字符c
  string s4(s2);
  //string(const string &s)拷贝构造函数
  string s5(s3,5);
  //string(const string &s,size_t n)
  //用s中的前n个字符构造新的string类对象
  cout<<s1<<endl;
  cout<<s2<<endl;
  cout<<s3<<endl;
  cout<<s4<<endl;
  cout<<s5<<endl;
  return 0;
}

有5种常见构造,需要注意的是拷贝构造函数这一种。

https://www.cnblogs.com/wangguchangqing/p/6141743.html这是一位大佬写的拷贝构造和赋值运算符的一些知识。

我不是很懂const char* s,char const * s,char* const s;

首先,const char* s,根据运算符优先级,*s首先先结合,代表指针,char代表指针类型,p指向的内容为const类型不可修改。

char const* s首先没有const*这种类型,所以const 修饰的和前面的是一样的,char const* s和const char* s是一样的。

char* const s 定义一个指向字符的指针常数,即const指针,不能修改指针,但能修改指针指向的内容。

https://blog.youkuaiyun.com/m0_37806112/article/details/81252151

https://blog.youkuaiyun.com/SilentOB/article/details/76994618这两篇博客讲述的很清楚。

2.string类对象的容量操作。

#include<iostream>
#include<cstdio>
#include<string>
using namespace std;
int main()
{
  string s("hello world!");
  cout<<s.size()<<endl;
  cout<<s.length()<<endl;
  cout<<s.capacity()<<endl;
  cout<<s<<endl;
  s.clear();
  //清空时只是将size清0,并不改变底层空间的大小也就是说容量大小不变
  cout<<s.size()<<endl;
  cout<<s.capacity()<<endl;
  s.resize(10,'a');
  cout<<s.size()<<endl;
  cout<<s.capacity()<<endl;
  cout<<s<<endl;
  s.resize(25);//此时的有效字符个数增加到25个,多余的用'\0'来填充
  cout<<s.size()<<endl;
  cout<<s.capacity()<<endl;
  cout<<s<<5<<endl;
  s.resize(10);//此时的有效字符个数减少到10个,多余的用'\0'来填充
  cout<<s.size()<<endl;
  cout<<s.capacity()<<endl;
  cout<<s<<5<<endl;
  string s2;
  s2.reserve(100);
  cout<<s2.size()<<endl;
  cout<<s2.capacity()<<endl;
  s2.reserve(50);
  cout<<s2.size()<<endl;
  cout<<s2.capacity()<<endl;
  return 0;
}

运行结果可以自己跑一下,有几点需要注意的。

  1. size()和length()是一样的,都是求长度,底层原理实现相同。
  2. clear()是将字符串清空操作,清空后的字符串size变为0,但是它的底层容量大小是不变的。
  3. resize(size_t n)与resize(size_t n,char c)都是将字符串的有效字符改到n个,不同的是当字符个数增多时,resize(size_t n)用'\0'来增添多出的元素空间,resize(size_t n,char c)字符个数增多时,是用c来填充多出的元素空间。
  4. resize在改变元素个数时,如果将元素个数增多,可能会改变底层容量的大小,就是说capacity()的值会发生改变。如果将元素个数减小,底层空间总大小不变。
  5. reserve(size_t res=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserve不会改变容量大小。任何操作都不会导致空间缩小,capacity只会增大,不会缩小。
  6. empty()的用法检查字符串是否为空串,是返回true,非空返回false。

3.string类对象的访问操作。

访问操作是比较简单的。

4.string类对象的修改操作。

#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
int main()
{
  string s1("hello world!");
  //void push_back(char c)在字符串后尾插字符c
  s1.push_back('u');
  cout<<s1<<endl;
  s1.append(" love me");
  //string& append(const char* s)在字符串后追加一个字符串
  cout<<s1<<endl;
  s1+='b';
  //string &operator+=(char c)在字符串后面追加字符c
  cout<<s1<<endl;
  s1+="it";
  //string &operator+=(const string &str)在字符串后面追加字符串str
  cout<<s1<<endl;
  //const char* c_str()const返回c风格字符串
  cout<<s1.c_str()<<endl;
  return 0;
}

这是一些基础的知识点。

比价重要的是find函数。我太难了!我昨天写这方面的博客,查资料各方面,弄了很久,我太难了。而且我特别记得保存了,但是凉了。

find函数:

1.字符 2字符串 3.字符容器。

#include<iostream>
#include<cstdio>
#include<string>
using namespace std;
int main()
{
  string str("There are two needles in this haystack with needles.");
  string str2("needle");
  size_t found=str.find(str2);
  if(found!=string::npos)
  {
    cout<<"first 'needle' found at: " << found <<endl;
  }
  string s1("hello world!Do you love me?");
  string s2("mello");
  size_t ans=s1.find(s2);
  cout<<ans<<endl;
  string s3("world");
  size_t ans2=s1.find(s3,2);
  cout<<ans2<<endl;
  return 0;
}

缺省参数:从什么位置开始找,找到了返回下标,找不到返回-1,但是由于返回值的类型是unsigned类型,所以打印出来的是4294967295.   string::npos,npos代表的是找不到。

rind函数和find函数的用法类似,只是rfind函数是从后往前查找。

substr函数是查找子串。

string尾部追加字符时需要注意一些点。对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。

5.string类非成员函数。

operator+,operator>>,operator<<,getline,relational operators(大小比较)这几个接口也需要着重了解。

下面贴几道leetcode的题目。

经典的string类问题

string 类的常见问题:string类的构造,拷贝构造,赋值运算符重载以及析构函数。

https://blog.youkuaiyun.com/u011000290/article/details/49020335

大佬把拷贝构造,构造,析构函数,赋值函数很清楚。大佬不愧是大佬。

 https://juejin.im/post/5b5dcf8351882519790c9a2e 深浅拷贝的具体区别

每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其他的称为普通构造函数)。对于任意一个类A,如果不手动编写上述函数,C++编译器将自动为A生成四个缺省的参数。就是四个缺省的构造,拷贝构造,析构,赋值函数。

但是很有必要自己手动书写以上几个函数。因为:

(1)如果使用“缺省的无参构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会。

(2)缺省的拷贝构造函数和缺省的赋值函数均采用浅拷贝而非深拷贝的方式来实现。浅拷贝(位拷贝)只是将值拷贝过来,会导致共用同一对象问题,倘若类中含有指针变量,再去访问的话,如果原来的已经销毁,那么将会出错(发生访问违规现象)。

注意点:拷贝构造函数函数入口时不需要与null进行比较,因为引用不可能为空。

                                  不主动编写构造函数和赋值函数,浅拷贝因为此处的数据类型是指针,所以拷贝的也是地址,若系统调用自己的构造函数,容易出现共用同一内存地址,虽然指向的内容是一样的,但是会发生错误:内存泄露;共用同一内存,特别容易受影响;析构释放两次。

#include<iostream>
#include<cstdio>

#include<cstring>
#include<algorithm>
using namespace std;
class String
{
public:
  String(const char* str=NULL);//普通构造函数
  String(const String &other);//拷贝构造函数
  ~String(void);//析构函数
  String &operator = (const String &other);//赋值函数
private:
  char *m_data;//保存字符串
};
String::String(const char *str)
{
  if(str==NULL)
  {
    m_data=new char[1];//对空字符串自动申请存放结束标志'\0'的空
    *m_data='\0';//对m_data加NULL判断
  }
  else
  {
    int length=strlen(str);
    m_data=new char[length+1];
    strcpy(m_data,str);//把str复制到m_data数组里面
  }
}
String::~String(void)
{
  delete[] m_data;
  m_data=nullptr;
}
String::String(const String &other)
{
  int length=strlen(other.m_data);
  m_data=new char[length+1];
  strcpy(m_data,other.m_data);
}
//String::String &operator=(const String &other)
String& String::operator=(const String &other)
{
  if(this==&other)//检查自赋值
  {
    return *this;
  }
  delete[] m_data;//释放原有的内存
  int length=strlen(other.m_data);
  m_data=new char[length+1];//对m_data加NULL判断
  strcpy(m_data,other.m_data);
  return *this;//返回本对象的引用
}
int main()
{
  return 0;
}

浅拷贝与深拷贝以及写时拷贝 

浅拷贝:也称位拷贝,编译器只是将对象中值拷贝过来,如果对象中管理资源,就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放掉,以为还有效,所以当继续对资源进行操作时,就会发生了访问违规。

深拷贝:如果一个类中涉及到资源管理,其拷贝构造函数,赋值运算符重载以及析构函数都要必须要显式给出,一般情况下都是按照深拷贝方式。深拷贝给每个对象独立分配资源,保证多个对象之间不会因共享资源而造成多次释放造成程序崩溃问题。

写时拷贝:在浅拷贝的基础上引用计数的方式实现。引用计数就是用来记录资源使用者的个数。在构造时,将资源的计数给成一,每增加一个对象使用该资源,就给计数加一,当某个对象被销毁时,计数减一,然后再检查是否需要释放资源,如果计数为一,则说明该对象是资源的最后一个使用者,将该资源释放,否则就不能释放,因为还有其他对象在使用该资源。

string类的模拟实现

自己手动实现string类。代码量好大,估计400到500行。不想写。但是一定要写啊,苍天饶过谁。

string类相关练习

 大概10道leetcode上面关于string类的练习。

class Solution {
public:
    void reverseString(vector<char>& s) {
        int len=s.size()-1;
        for(int i=0,j=len;i<j;i++,j--)
        {
            swap(s[i],s[j]);
        }
    }
};

很简单,但是应该要注意一点的是条件表达式的写法,,有人可能会写成i<=len,j>=0,这样写就根本没有交换,也就是执行了两次交换。一定要是i<j.这样子就没有问题了。

顺便提醒一下,string函数里,是有reverse函数的,基于迭代器的函数,使用方法reverse(s.begin(),s.end())

https://blog.youkuaiyun.com/u012877472/article/details/49557077

直接调用库里面的函数当然是更快的。

 其实也很简单。

class Solution {
public:
    int firstUniqChar(string s) {
      int count[27]={0};
      for(auto &ch:s)
      {
          count[ch-'a']++;
      }
      for(int i=0;i<s.size();i++)
      {
          if(count[s[i]-'a']==1)//对应位置的字符的出现次数
          {
              return i;
          }
      }
        return -1;
    }
};

找出字符串中第一个只出现一次的字符串。因为都是小写字母直接减‘a'就可以。用count数组进行计数,从头遍历,出现次数最先为1的输出。思维逻辑就是这样。

题目描述

计算字符串最后一个单词的长度,单词以空格隔开。 

输入描述:

一行字符串,非空,长度小于5000。

输出描述:

整数N,最后一个单词的长度。

示例1

输入

复制

hello world

输出

复制

5

思路很简单,查找最后一个空格,rfind函数正好是逆序查找,所以先查找最后一个空格的位置,然后用总长度-pos-1就是最后一个单词的长度。

 

#include<iostream>
#include<cstdio>
#include<string>
#include<algorithm>
using namespace std;
int main()
{
    string str;
    getline(cin,str);
    size_t pos=str.rfind(' ');
    cout<<str.size()-pos-1<<endl;
    return 0;
}

判断回文,这是一个很经典的判断回文问题 ,根据题目要求,只考虑数字和字母字符,所以就要写一个函数判断是否是数字和字母字符,另外又提到忽略字母的大小写,所以要写一个全部转换为小写字母或者大写字母的函数,然后双指针法判断回文。这样子是比较传统的一个做法,时间复杂度相对来说不是很高。但是其实库里面有这两个函数在ctype.h里面,有判断是否是数字和字母字符的isalnum函数还有tolower函数,全部转化为小写字母,用库里面的函数时间复杂度会很低,因为是库里面自带的,当然是最好的。

class Solution {
public:
    bool isPalindrome(string s) {
     int st=0;
     int ed=s.size()-1;
     while(st<=ed)
     {
         if(!isalnum(s[st]))
         {
             st++;
             continue;
         }
         if(!isalnum(s[ed]))
         {
             ed--;
             continue;
         }
         if(tolower(s[st])!=tolower(s[ed]))
         {
             return false;
         }
         st++;
         ed--;
     }
    return true; 
  }
};

LeetCode回文数题目集锦 - 一根稻草大佬对回文题目的总结。

https://leetcode-cn.com/problems/reverse-string-ii/

区间部分翻转。有一点需要注意的:注意判断不合理状况以及栈溢出状况,另一点是左端点的值是一直更新的,因为k的值是不同的,所以st+=2*k请注意题目描述。

class Solution {
public:
    string reverseStr(string s, int k) {
      if(s.size()<1||k<1)  return s;
      int st=0,len=s.size();
      while(st<=len-2*k)
      {
          reverse(s.begin()+st,s.begin()+st+k);
          st+=2*k;
      }
      int res=len-st;
      if(res<k)
      {
          reverse(s.begin()+st,s.end());
      }
      else if(res>=k&&res<2*k)
      {
          reverse(s.begin()+st,s.begin()+st+k);
      }
      return s;
    }
};

 

 

https://leetcode-cn.com/problems/reverse-words-in-a-string-iii/

反转单词,我个人的思路是查找空格并进行翻转,查找空格用rfind的方法,但是莫名其妙不行,很尴尬,总是有bug,还可以使用stringstream的方法,但是空间消耗不小。

class Solution {
public:
    string reverseWords(string s) {
     stringstream ss;
        ss << s;
        string v, n;
        while(ss >> n){
            reverse(n.begin(),n.end());
            v.append(n+' ');
        }
        v.pop_back();
        return v;
    }
};

 这个我不太会,看的是网上大佬的代码。

剩下的string类题目以后会补充,目前就这些,本章节主要内容在于string里面的基本用法以及函数的相关用法,无参构造,拷贝构造,析构,赋值运算符的实现方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值