DAY7 字符串(一):字符串基础+反转字符串

本文详细介绍了C++中的字符串基础,包括字符与整数的关系、字符数组的声明与操作、读取字符串的方法如fgets的使用。还讨论了标准库中的string类型,包括其定义、初始化、读写、比较以及操作字符的方法。文章还涵盖了字符串反转的思路和注意事项,以及处理无符号整数和下溢问题的要点。
摘要由CSDN通过智能技术生成

1.字符串基础部分

字符与整数的联系

字符与整数的联系——ASCII码
在ASCII编码中,每个字符都对应一个0到127的整数。注意:目前负数没有与之对应的字符。ASCII表没有负数。

#include <iostream>

using namespace std;

int main()
{
    char c = 'a';
    cout << (int)c << endl;

    int a = 66;
    cout << (char)a << endl;

    return 0;
}

常用ASCII值:‘A’- 'Z’是65 ~ 90,‘a’ - 'z’是97 - 122,0 - 9是 48 - 57。
字符可以参与运算,运算时会将其当做整数:

#include <iostream>

using namespace std;

int main()
{
    int a = 'B' - 'A'; //直接运算即可
    int b = 'A' * 'B';
    char c = 'A' + 2;

    cout << a << endl;
    cout << b << endl;
    cout << c << endl;

    return 0;
}

字符数组

字符串就是字符数组加上结束符’\0’

可以使用字符串来初始化字符数组,但此时要注意,每个字符串结尾会暗含一个’\0’字符,因此字符数组的长度至少要比字符串的长度多 1 !

#include <iostream>

using namespace std;

int main()
{
    char a1[] = {'C', '+', '+'};            // 列表初始化,没有空字符
    char a2[] = {'C', '+', '+', '\0'};      // 列表初始化,含有显示的空字符
    char a3[] = "C++";                      // 自动添加表示字符串结尾的空字符
    char a4[6] = "Daniel";                  // 错误:没有空间可以存放空字符

    return 0;
}
字符数组的输入和输出
#include <iostream>

using namespace std;

int main()
{
    char str[100];

    cin >> str;             // 输入字符串时,遇到空格或者回车就会停止
    cout << str << endl;    // 输出字符串时,遇到空格或者回车不会停止,遇到'\0'时停止
   // printf("%s\n", str);

    return 0;
}
读入字符串
#include <iostream>

using namespace std;

int main()
{
    char str[100];

    fgets(str, 100, stdin);  // gets函数在新版C++中被移除了,因为不安全。
                             // 可以用fgets代替,但注意fgets不会删除行末的回车字符

    cout << str << endl;

    return 0;
}
fgets的用法

fgets是一个在C和C++中常用的标准库函数,用于从文件或流中读取一行文字。这个函数通常用于从控制台读取用户输入,或者从文件中读取数据。

在上面的代码中,fgets(str, 100, stdin); 的意思是从标准输入(通常是键盘输入)读取最多99个字符(包括空格和制表符)到str数组中,然后在字符串末尾自动添加一个**空字符(‘\0’)**以标记字符串的结束。

关于函数参数:

  • str是存储输入的字符数组或字符指针。
  • 100表示要读取的最大字符数,包括最后的空字符(‘\0’),所以实际上只会读取99个字符。
  • stdin表示标准输入流,通常是键盘输入。

值得注意的是,fgets保留输入的换行符(如果在读取的字符数范围内)。也就是说,如果用户在输入完成后按下回车键,这个回车(‘\n’)也会被读入到str中。如果你不想在字符串中保留这个换行符,需要手动删除。

gets函数存在安全隐患,它不能防止缓冲区溢出,如果输入的字符超过了预期,可能会导致程序崩溃或其他安全问题。所以现在推荐使用fgets代替。)

字符数组常用操作

下面几个函数需要引入头文件:

#include <string.h>

(1) strlen(str),求字符串的长度
(2) strcmp(a, b),比较两个字符串的大小,a < b返回-1,a == b返回0,a > b返回1。这里的比较方式是字典序!
(3) strcpy(a, b),将字符串b复制给从a开始的字符数组。

#include <iostream>
#include <string.h>

using namespace std;

int main()
{
    char a[100] = "hello world!", b[100];

    cout << strlen(a) << endl;

    strcpy(b, a);

    cout << strcmp(a, b) << endl;

    return 0;
}

字符数组的遍历

#include <iostream>
#include <string.h>

using namespace std;

int main()
{
    char a[100] = "hello world!";

    // 注意:下述for循环每次均会执行strlen(a),运行效率较低,最好将strlen(a)用一个变量存下来
    for (int i = 0; i < strlen(a); i ++ )
        cout << a[i] << endl;

    return 0;
}

标准库类型string

可变长的字符序列,比字符数组更加好用。需要引入头文件:

#include <string>
定义与初始化
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string s1;              // 默认初始化,s1是一个空字符串
    string s2 = s1;         // s2是s1的副本,注意s2只是与s1的值相同,并不指向同一段地址
    string s3 = "hiya";     // s3是该字符串字面值的副本
    string s4(10, 'c');     // s4的内容是 "cccccccccc",这里真的是10个c,没有结束符'\0'

    return 0;
}
string的读写
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string s1, s2;

    cin >> s1 >> s2;
    cout << s1 << s2 << endl;

    return 0;
}
使用getline读取一整行
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string s;

    getline(cin, s);

    cout << s << endl;

    return 0;
}
string的empty和size操作(注意size是无符号整数,因此 s.size() <= -1一定成立):
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string s1, s2 = "abc";

    cout << s1.empty() << endl;
    cout << s2.empty() << endl;

    cout << s2.size() << endl;

    return 0;
}
string.size()无符号整数取值

在C++中,s.size()的返回类型是size_t,这是一个无符号整数类型。因此,它不能表示负数

如果你试图将其与**-1(一个有符号整数)进行比较,-1会被隐式转换为一个无符号整数**,即最大的无符号整数值(在32位系统中是4294967295,在64位系统中是18446744073709551615)。所以,无论s.size()是多少,s.size() <= -1这个比较的结果都将是true

如果需要检查一个字符串是否为空,你可以使用s.empty(),这个方法会返回一个布尔值,表示字符串是否为空。

(string.size类型是size_t,一般是32位或者64位无符号整数;而-1是int类型,在与unsigned int(也可能是unsigned long等等,看你具体编译器)进行比较时会被提升为相应的无符号类型,而-1的二进制补码是全1,把全1的二进制码当做无符号数解释的时候是无符号数的最大值,所以-1是最大的。)

string的比较

支持 >, <, >=, <=, ==, !=等所有比较操作,按字典序进行比较。

为string对象赋值
string s1(10, 'c'), s2;     // s1的内容是 cccccccccc;s2是一个空字符串
s1 = s2;                    // 赋值:用s2的副本替换s1的副本
                            // 此时s1和s2都是空字符串
两个string对象相加
string s1 = "hello,  "", s2 = "world\n";
string s3 = s1 + s2;                    // s3的内容是 hello, world\n
s1 += s2;                               // s1 = s1 + s2
字面值和string对象相加

做加法运算时,字面值和字符都会被转化成string对象,因此直接相加就是将这些字面值串联起来:

string s1 = "hello", s2 = "world";      // 在s1和s2中都没有标点符号
string s3 = s1 + ", " + s2 + '\n';

当把string对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符两侧的运算对象至少有一个是string:

(特别注意下面例子中最后一个)

string s4 = s1 + ", ";  // 正确:把一个string对象和有一个字面值相加
string s5 = "hello" + ", "; // 错误:两个运算对象都不是string

string s6 = s1 + ", " + "world";  // 正确,每个加法运算都有一个运算符是string
string s7 = "hello" + ", " + s2;  // 错误:不能把字面值直接相加,运算是从左到右进行的
处理string对象中的字符

可以将string对象当成字符数组来处理:

#include <iostream>
#include <string>

using namespace std;

int main()
{
    string s = "hello world";

    for (int i = 0; i < s.size(); i ++ )
        cout << s[i] << endl;

    return 0;
}

或者使用基于范围的for语句:

#include <iostream>
#include <string>

using namespace std;

int main()
{
    string s = "hello world";

    for (char c: s) cout << c << endl;

    for (char& c: s) c = 'a';

    cout << s << endl;

    return 0;
}

344.反转字符串

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

示例 1:

输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]

思路

注意:本题目要求不给数组分配额外的空间!也就是说翻转操作必须原地进行,时间复杂度需要是O(1)

翻转的过程是头尾各有一个指针,两个指针同时向中间移动。

(数组中有序数组平方和也是双指针往中间移动,哈希表的四数之和/三数之和题目也涉及了类似的思想,双指针回头做一个整理)

伪代码

len = nums.size();
for(i=0,j=len-1;i<len/2;i++,j--)  //这里的j不要用nums.size()-1,因为有可能会发生下溢,后面做了解释
    //这里i<len/2而不是i<=len/2的原因,下面有整理
    //如果是双指针同时移动,可以同时写在for循环里,这也是一种写法
{
    swap(nums[i],nums[j]); //库函数如果不影响题目本意,可以调用
}
    
i<len/2而不是i<=len/2的原因

拿不准边界条件的时候可以自己代入数据尝试。

假设数组长度为5,数组下标即为0 1 2 3 4 ,此时i的循环条件是i<2 (5/2=2),即前半部分的i是0 1,满足条件(中间的2不需要交换)。

假设数组长度为4,数组下标即为0 1 2 3,此时i的循环条件是i<2,也就是取值为0 1,也符合前半部分的条件,所以边界条件是i<len/2

如果是i<=len/2,那么长度为5的时候,前半部分取值成为0 1 2,但是中间的2是不需要交换的。

分析边界条件的时候一定要注意,数组长度为5的时候,数组下标是01234,没有5,i的取值也是01234

库函数什么时候能用?

如果库函数一步就把题目解决,就不能用,但是如果库函数只是解题一部分,并且大致了解库函数时间复杂度与逻辑等信息,就可以直接用。

完整版

class Solution {
public:
    void reverseString(vector<char>& s) {
        int len = s.size();
        int j=len-1;  //注意j的赋初值一定要写在外面
        for(int i=0;i<len/2;i++){
            swap(s[i],s[j]);
            j--;
        }

    }
};

整理关于num.size()无符号整型和下溢的问题

例子1:

下面这段代码,改成n=nums.size(),i<n-3,就可以过,i<nums.size()-3就过不了。
在这里插入图片描述
原因:nums.size() 是无符号整形,nums.size() - 3会变成18446744073709551614

nums.size() 的结果是一个无符号整数类型,假设其值为 3。当你执行 nums.size() - 3,实际上是 3 - 3,结果为 0。但是,如果你使用 nums.size() - 3,那么实际上是 3 - 3 被转换为 3 + (UINT_MAX - 2),其中 UINT_MAX 是无符号整数类型的最大值。因此,nums.size() - 3 的结果为 UINT_MAX - 2

对于无符号整数类型来说,UINT_MAX 的值通常是 2^N - 1,其中 N 是该类型的位数。对于一个 64 位无符号整数类型,UINT_MAX 的值是 2^64 - 1,这是一个非常大的数,约为 18446744073709551615

所以,nums.size() - 3 的结果为 UINT_MAX - 2,在你的例子中是 18446744073709551614

这种情况下,如果你希望得到正确的结果,你可以进行显式的类型转换,将结果转换为有符号整数类型,或者重新考虑你的算法逻辑,避免无符号整数的溢出问题。

也就是说c++里nums.size()这种无符号整数,不能直接和3这种整数相减,必须先int n = nums.size()才行,先用个 int 变量赋值。

例子2:

仅仅是i<nums.size()是没有问题的
在这里插入图片描述

例子3:
在这里插入图片描述

总结
  • std::vector::size()返回的是size_type类型,这在大多数情况下是一个无符号整数。如果我们的**int i负数**,i < nums.size()这样的比较可能会引发问题,因为这时i会被提升为无符号整数,从而可能导致不符合预期的结果。

  • 然而,i始终为非负数(如在常见的循环中),那么i < nums.size()就不会出现问题,因为两者都是非负的。

  • 当我们写i < nums.size() - 3时,如果nums.size()的值小于3,那么nums.size() - 3将会发生下溢,即减去3会导致值变成一个非常大的正数(因为无符号整数不能表示负数)。这样的话,i再与nums.size() - 3进行比较就会出现问题。在这种情况下,一个更安全的做法是写i + 3 < nums.size()(或者先用int变量赋值),这样就不会有下溢的问题。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值