#include <cctype>
#include <iostream>
#include <string>

using namespace std;

int main() {

    string str;
    getline(cin, str);

    int whitespace = 0;
    int digits = 0;
    int chars = 0;
    int others = 0;

    // write your code here......
    for(int i=0; i<str.length(); i++){
        if(isalpha(str[i])){
            chars++;
        }else if(isdigit(str[i]))
        {
            digits++;
        }else if(isspace(str[i])){
            whitespace++;
        }else {
            others++;
        }
    }

    cout << "chars : " << chars
        << " whitespace : " << whitespace
        << " digits : " << digits
        << " others : " << others << endl;

    return 0;
}

核心问题:为什么 string 对象可以直接用 str[i]

能直接用 str[i] 访问第i个字符,最核心的原因只有一个,也是 C++ 的核心特性:

C++ 标准库中的 std::string 类,对「数组下标运算符 [] 做了运算符重载(operator [] 重载)」

✅ 关于「运算符重载」的通俗解释

在 C++ 中,[] 本来是给「原生字符数组」用的,比如:

cpp

运行

// 原生char数组,天生支持 arr[i] 访问
char arr[] = "hello";
cout << arr[0]; // 输出 'h',合法

string 本质是一个类 (class),不是原生数组,类的对象本身是不能直接用 [] 的。

C++ 的设计者为了让 std::string 用起来像原生字符数组一样简洁、易用,就在 std::string 类的内部,提前实现了 operator[] 这个重载函数,把「下标访问字符」的逻辑封装好了。

string::operator[] 重载的具体作用

这个重载的 [] 运算符,规则是:

  1. 接收一个非负整数下标 i 作为参数;
  2. 返回字符串中「第i个字符的引用(char&)」;
  3. 下标从 0 开始计数(和原生数组一致):str[0] 是第一个字符,str[1] 是第二个字符,以此类推;
  4. 既能读取字符,也能修改字符。

举个合法使用的例子:

cpp

运行

#include <iostream>
#include <string>
using namespace std;

int main() {
    string str = "hello"; // 初始化带内容的字符串
    // 1. 读取:通过[]访问指定下标字符
    cout << str[0] << endl; // 输出 'h'
    cout << str[3] << endl; // 输出 'l'
    
    // 2. 修改:通过[]直接修改指定下标字符
    str[1] = 'i'; 
    cout << str << endl; // 输出 "hillo",修改生效
    
    return 0;
}

三、底层原理:string 为什么能实现这个重载?

你可能会好奇,string 内部是怎么存字符的,才能支持下标访问?

std::string 的底层本质,就是封装了一个「动态可扩展的 char 类型字符数组」

字符串的所有字符,在内存中是连续存储的,string 类帮我们管理了这个数组的内存申请、扩容、释放。

operator[] 重载函数的底层逻辑,就是拿着你传入的下标 i,去访问这个内部 char 数组的第 i 个元素,再把结果返回给你。

简单说:string 是「披着类的外衣,内部是字符数组」,再通过运算符重载,让我们能用数组的语法访问,兼顾了易用性和安全性。

三、⚠️ 重中之重:str[i] 的「致命坑点」+ 必知规则(新手 99% 踩坑)

这是你提问里隐含的最关键注意事项,也是 C++ 中使用 str[i] 最容易出错的地方,一定要牢记

规则 1:std::stringoperator[] 运算符,不做下标越界检查

什么是「下标越界」?

对于一个字符串 str,合法的下标范围是:0 ≤ i < str.size()

  • 字符串的有效字符数量是 str.size(),下标最大只能到 str.size()-1
  • 如果你的下标 i < 0 或者 i ≥ str.size(),就属于越界访问

越界访问的后果是什么?

C++ 编译器不会报错,程序能编译通过,但运行时会触发 未定义行为 (Undefined Behavior)

  • 可能输出乱码、随机字符;
  • 可能导致程序直接崩溃(闪退);
  • 可能程序暂时没事,但埋下隐藏 bug,后续莫名其妙出错。

✅ 你最可能踩的坑:string str; 空字符串的 str[i] 绝对非法!

回到开头的 string str; —— 这是一个空字符串,它的 str.size() = 0,合法的下标范围是空的(没有任何合法下标)。

此时任何形式的 str[i] 都是越界访问,比如:

cpp

运行

string str; // 空字符串,size=0
cout << str[0]; // 非法!越界访问,未定义行为
str[0] = 'a';   // 非法!越界赋值,未定义行为

这是新手最容易犯的错误,一定要避免!

四、安全的替代方案:str.at(i) 成员函数

既然 str[i] 不做越界检查,很危险,那有没有安全的下标访问方式?

答案是:有!使用 std::string 的成员函数 str.at(i)

str.at(i)str[i] 的对比

✅ 相同点:

  • 下标计数规则一致:从 0 开始,str.at(0) 是第一个字符;
  • 功能一致:既能读取字符,也能修改字符;

✅ 核心不同点(关键优势):

str.at(i) 会做「严格的下标越界检查」

越界时的差异演示:

cpp

运行

#include <iostream>
#include <string>
using namespace std;

int main() {
    string str = "hello"; // size=5,合法下标0~4
    
    // 情况1:str[i] 越界,无检查,程序运行崩溃/乱码
    cout << str[10]; // 越界,未定义行为,危险!
    
    // 情况2:str.at(i) 越界,主动抛出异常,程序可控
    cout << str.at(10); // 编译器检测到越界,直接抛出 std::out_of_range 异常
    return 0;
}

结论:如何选择?

  1. 追求极致效率,且能 100% 保证下标合法 → 用 str[i]
  2. 日常开发、新手学习、不确定下标是否合法 → 优先用 str.at(i),虽然效率略低一点点,但能避免致命的越界 bug,性价比极高。

✅ 总结(所有知识点浓缩,建议收藏)

  1. string str; → 对std::string默认初始化,得到一个空字符串(长度 0,无字符);
  2. str[i] 可用的核心原因 → std::string重载了数组下标运算符 operator[]
  3. string 底层本质 → 封装了连续存储的 char 类型动态数组[] 本质是访问这个数组的第 i 个元素;
  4. 核心禁忌 → 空字符串(size=0)的任何 str[i] 都非法;下标 i >= str.size() 也是非法;
  5. 安全原则 → 新手用 str.at(i) 替代 str[i],能自动检查下标越界,规避 99% 的下标错误;
  6. 下标规则 → 永远从 0 开始,最后一个字符的下标是 str.size()-1

希望以上内容能帮你彻底理清这个知识点,这是 C++ string 最基础也是最核心的用法之一 ✨