第10讲:字符串

《计算机程序设计》

苏醒

计算机学院计算机研究所编译系统研究室

课前准备

  • 在WSL Ubuntu中建立一个专门的目录用于课上练习,如~/course
$ mkdir ~/course
  • 打开VSCode并连接到WSL,打开此目录并新建文件,如~/course/main.cpp
  • 编辑main.cpp,随堂编程练习的代码请直接在此文件中编辑
main.cpp
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
  // ============= begin =============

  // =============  end  =============
  return 0;
}

温故

测一测

判断下列程序的输出

ptrquiz.cpp
#include <iostream>
int *trickme(int *p) {
  ++*(p++);
  return p;
}
int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  int *p = arr, sum = 0;
  trickme(trickme(p));
  for (int i = 0; i < 5; ++i)
    sum += arr[i];
  std::cout << sum << std::endl;
  return 0;
}
$ g++ -o code/ptrquiz code/ptrquiz.cpp
$ ./code/ptrquiz

敏感词检测

敏感词检测

在文字中检测给定的敏感词,分析其出现的位置与次数。

That was yet unnamed warship was built in a Dalian shipyyard next to where the Liaoning is berthed and began a final sea trial in the Bohai Sea on Sunday, heading out to the Yellow Sea. Its design is based on the Liaoning but with a number of modifications.

keyword.cpp
#include <iostream>
#include <cstring>

int main() {
  char text[] = "That was ...", keyword[] = "Liaoning";
  int count = 0;
  char *positions[10];
  // ============= begin =============
  // 在text中找到keyword所有出现的位置,存放在positions数组中,并统计出现次数存放在count中
  // =============  end  =============
  std::cout << "count = " << count << std::endl;
  for (int i = 0; i < count; i++) {
    std::cout << "positions[" << i << "] = " << positions[i] << std::endl;
  }
  return 0;
}

学习目标

  • 学习内容
    • 指针与数组
    • 字符串
    • 字符串函数

学习目标

  • 通过数组与指针的互换性更加深刻地理解地址
  • 掌握字符串、字符数组、字符指针的区别与联系
  • 使用字符串函数解决实际问题

应用本节知识,解决敏感词分析问题!

一、指针与数组

指针与数组的本质(复习)

考考你

指针的本质是什么?数组名的本质又是什么?二者的区别在哪里?

  • 指针与数组名的本质都是带类型的地址
  • 二者的区别在于,指针是变量而数组名是常量
int a[10];
int *p = &a[0];
p += 1;    // 指针可以修改
// a += 1;  // 编译错误

明察秋毫

指针与数组具有很强的可操作性,在相当程度上具有互换性

1.1 数组作为指针

  • 数组名可看作一个常量指针,支持指针的运算
arrayasptr.cpp
#include <iostream>
#include <iomanip>
int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  *arr = -1;         // 解引用操作符可以作用于数组名;
  std::cout << *arr << std::endl; // 输出-1
  int *p = arr;      // 数组可以直接赋值给指针,等价于p = &arr[0]
  int *q = arr + 1;  // 数组名可以与整数进行加减运算(以元素类型为单位),得到新的地址
  std::cout << std::boolalpha << (arr == p) << std::endl; // 数组名支持比较运算
  return 0;
}

1.2 指针作为数组

  • 指针支持索引运算符[],可看作一个数组的首地址
ptrasarray.cpp
#include <iostream>
#include <iomanip>
int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  int *p = arr;      // 数组可以直接赋值给指针,等价于p = &arr[0]
  for (int i = 0; i < 5; i++)
    std::cout << p[i] << std::endl; // 输出数组元素
  return 0;
}

明察秋毫

通过索引运算符访问数组元素,等价于对指针进行偏移后解引用。即p[i]等价于*(p + i)

指针与数组小结

  • 设指针p指向数组a的首元素,p + i等价于a + i,则有以下表达式等价
数组与指针的互换性
表达式 等价表达式 含义
p = a p = &a[0] 指针p指向数组a首地址
a[i] *(p + i) 数组a的第i个元素,亦即指针p之后的第i个元素
*(a + i) p[i] 数组a的第i个元素,亦即指针p之后的第i个元素

指针与数组练习

测一测

已知pa分别是一个int *指针与一个int数组,下列哪些表达式是合法的?

  1. p = a
  2. p = &a[0]
  3. a = p
  4. p = a+1
  5. a = p+1
  6. ++p
  7. a[1] = p[-1]

二、字符串

字符串字面量(复习)

  • 字符串字面量是以双引号(")括起来的字符序列,如"hello world"
  • 特殊字符在字符串中需要转义,如\n表示换行符
strliteral.cpp
#include <iostream>

int main() {
  std::cout << "hello, world\n";
  return 0;
}

字符串的存储(复习)

  • 字符串在内存中连续存储,每个字符占一个字节
  • 字符串尾部以空字符\0作为结束标志,额外占一个字节
const char* hello = "Hello World!\n"; // 长度为13,在内存中占14个字节
const char* empty = "";               // 长度为0,在内存中占1个字节

C++字符串在内存中的存储方式

考考你

如果希望在程序中对字符串进行处理,应该将字符串存储在什么结构中?

2.1 字符数组

  • 字符串需要连续存储,使用数组是最为最为自然的选择
chararray.cpp
#include <iostream>
using namespace std;
int main() {
  char s0[4] = {'a','b','c','d'}; // 使用初始化列表初始化字符数组
  char s1[4] = "ef";  // 使用字符串字面量初始化字符数组
  char s2[] = "ghi";  // 使用字符串字面量初始化字符数组,长度自动计算
  cout << sizeof(s2) << endl;
  cout << s0 << endl; // 对于一般类型的数组名(指针),输出的是地址
  cout << s1 << endl; // 但对于char数组(指针),输出的是以该地址起始的字符串
  cout << s2 << endl;
  return 0;
}
$ g++ -o code/chararray code/chararray.cpp
$ ./code/chararray
4
abcdef
ef
ghi

考考你

  • 为何s2的长度为4而不是3?
  • 为何输出0的结果不是abcd
  • s1[2]s1[3]的值是什么?

2.2 字符数组的初始化

chararrayinit.cpp
#include <iostream>
using namespace std;
void printCharArray(char s[], int n) {
  for (int i = 0; i < n; i++)
    cout << (s[i] ? s[i] : '#') << " ";
  cout << endl;
}
int main() {
  char s0[4] = {'a', 'b', 'c', 'd'};
  char s1[4] = {'a', 'b', 'c'};
  char s2[4] = "ef";
  char s3[] = "ghi";
  // char s4[4] = "jklm"; // 编译错误,试一试!
  printCharArray(s0, 4);
  printCharArray(s1, 4);
  printCharArray(s2, 4);
  printCharArray(s3, 4);
  return 0;
}
$ g++ -o code/chararrayinit code/chararrayinit.cpp
$ ./code/chararrayinit
a b c d 
a b c # 
e f # # 
g h i # 

明察秋毫

  • 字符数组初始化
    • 使用字符串字面量,自动添加'\0'
    • 使用初始化列表,不会自动添加'\0'

考考你

为什么s1采用初始化列表初始化,但尾部却添加了'\0'

2.3 字符数组与字符串

  • 字符数组通常用于保存字符串,但二者并不等价
    • 字符数组不一定以空字符结尾
    • 字符数组存储的字符串不一定用尽整个数组空间
chararray.cpp
#include <iostream>
using namespace std;
int main() {
  char s0[4] = {'a','b','c','d'}; // 使用初始化列表初始化字符数组
  char s1[4] = "ef";  // 使用字符串字面量初始化字符数组
  char s2[] = "ghi";  // 使用字符串字面量初始化字符数组,长度自动计算
  cout << sizeof(s2) << endl;
  cout << s0 << endl; // 对于一般类型的数组名(指针),输出的是地址
  cout << s1 << endl; // 但对于char数组(指针),输出的是以该地址起始的字符串
  cout << s2 << endl;
  return 0;
}

2.4 字符指针

  • C/C++对字符指针有特殊的处理——使用字符指针表示字符串
    • 例如,输入输出的特殊支持
strio.cpp
#include <iostream>
using namespace std;

int main() {
  char s[100];
  cin >> s;          // cin>>s修改s指向的内容而非s本身
  cout << s << endl; // cout<<s输出s指向的内容而非s本身
  int i[100];
  ///cin >> i;       // cin>>i试图修改i,然而i是常量,编译错误
  cout << i << endl; // cout<<i输出i的值,而非i指向的数组元素
  return 0;
}
$ g++ -o code/strio code/strio.cpp
$ echo 'Hello world!\n' | ./code/strio
Hello
0x7ffeab854eb0

字符串小结

  • 字符串以空字符结尾
  • 字符数组可以用于存储字符串
    • 字符数组如果未包含空字符,那么不能构成字符串
  • 字符指针用于指代一个字符串

字符串、字符数组与字符指针的关系

字符串练习

测一测

  • 仅给出整形数组的首地址(数组名或指针),能否确定数组的长度?
  • 仅给出字符串的首地址(字符数组或字符指针),能否确定字符串的长度?

三、字符串函数

C++字符串函数

  • C++标准库中提供了一组专门用于处理字符串的函数,这些函数声明在<cstring>头文件中
C++字符串函数
函数 功能
strlen(str) 计算字符串str的长度
strcpy(dst, src) 复制字符串src到字符串dst
strcat(dst, src) 追加字符串src到字符串dst尾部
strcmp(str1, str2) 按照字典序比较字符串str1str2的大小
strchr(str, ch) 在字符串str中查找字符ch
strstr(str, substr) 在字符串str中查找子串substr

3.1 strlen求字符串长度

  • 函数原型
size_t strlen(const char *str);
  • 返回字符串str的长度,不包括结尾的空字符
strlen.cpp
#include <iostream>
#include <cstring>
using namespace std;
int main () {
   const char *s = "Hello world";
   cout << strlen(s) << endl;
   return 0;
}
$ g++ -o code/strlen code/strlen.cpp
$ ./code/strlen
11

考考你

你能否实现自己的strlen函数?

3.2 strcpy复制字符串

  • 函数原型
char *strcpy(char *dest, const char *src);
char *strncpy(char *dest, const char *src, size_t n);
  • 将字符串src复制到字符串dest,返回dest
    • destsrc不能指向同一块内存
    • 对于strcpydest必须有足够的空间来保存src,否则会导致缓冲区溢出
    • strncpy最多复制n个字符
strcpy.cpp
#include <iostream>
#include <cstring>
int main () {
  char a[10], b[10] = "hello sun", c[10];
  strcpy(a, b);
  strncpy(c, b, 5);
  std::cout << a << '\n' << b << '\n' << c << '\n';
  return 0;
}
$ g++ -o code/strcpy code/strcpy.cpp
$ ./code/strcpy
hello sun
hello sun
hello@

考考你

你输出的字符串c是否和我相同?结尾有什么问题?

3.3 strcat字符串拼接

  • 函数原型
char *strcat(char *dest, const char *src);
char *strncat(char *dest, const char *src, size_t n);
  • 将字符串src追加到字符串dest尾部,返回dest
    • dest必须有足够的空间来保存src,否则会导致缓冲区溢出
    • strncatstrcat的区别在于,strncat最多复制n个字符
strcat.cpp
#include <iostream>
#include <cstring>
int main () {
  char a[10] = "hello", b[10] = "hello", c[10] = " sun";
  strcat(a, c);
  strncat(b, c, 3);
  std::cout << a << '\n' << b << '\n' << c << '\n';
  return 0;
}
$ g++ -o code/strcat code/strcat.cpp
$ ./code/strcat
hello sun
hello su
 sun

3.4 strcmp字符串比较

  • 函数原型
int strcmp(const char *str1, const char *str2);
  • 按照字典序比较字符串str1str2的大小
    • 返回值为0,表示两个字符串相等
    • 返回值大于0,表示str1大于str2
    • 返回值小于0,表示str1小于str2
strcmp.cpp
#include <iostream>
#include <cstring>
int main() {
  char s1[10], s2[10];
  std::cin >> s1 >> s2;
  int result = strcmp(s1, s2);
  char relation = result < 0 ? '<' : result > 0 ? '>' : '=';
  std::cout << s1 << relation << s2 << std::endl;
  return 0;
}
$ g++ -o code/strcmp code/strcmp.cpp
$ echo hello world | ./code/strcmp
hello<world

考考你

你能否实现自己的strcmp函数?

3.5 strchr字符查找

  • 函数原型
char *strchr(const char *str, int ch);
char *strrchr(const char *str, int ch);
  • 在字符串str中查找字符ch,返回第一次出现的位置
    • 如果未找到,返回nullptr
    • strchr从前往后查找,strrchr从后往前查找
strchr.cpp
#include <iostream>
#include <cstring>
int main() {
  char s[] = "hello world", c;
  std::cin >> c;
  char *first = strchr(s, c), *last = strrchr(s, c);
  if (first)
    std::cout << "first " << first - s << " last " << last - s << std::endl;
  else
    std::cout << "not found" << std::endl;
  return 0;
}
$ g++ -o code/strchr code/strchr.cpp
$ echo l | ./code/strchr
$ echo x | ./code/strchr
first 2 last 9
not found

3.6 strstr字符串查找

  • 函数原型
char *strstr(const char *str, const char *substr);
  • 在字符串str中查找子串substr,返回第一次出现的位置
    • 如果未找到,返回nullptr
strstr.cpp
#include <iostream>
#include <cstring>
int main() {
  char s[] = "hello world", subs[10];
  std::cin >> subs;
  char *first = strstr(s, subs);
  if (first)
    std::cout << "found at " << first - s << std::endl;
  else
    std::cout << "not found" << std::endl;
  return 0;
}
$ g++ -o code/strstr code/strstr.cpp
$ echo llo | ./code/strstr
$ echo sun | ./code/strstr
found at 2
not found

C++字符串函数小结

  • C++标准库中提供了一组专门用于处理字符串的函数,这些函数声明在<cstring>头文件中
C++字符串函数
函数 功能
strlen(str) 计算字符串str的长度
strcpy(dst, src) 复制字符串src到字符串dst
strcat(dst, src) 追加字符串src到字符串dst尾部
strcmp(str1, str2) 按照字典序比较字符串str1str2的大小
strchr(str, ch) 在字符串str中查找字符ch
strstr(str, substr) 在字符串str中查找子串substr

字符串函数练习:敏感词检测

敏感词检测

在文字中检测给定的敏感词,分析其出现的位置与次数。

keyword.cpp
#include <cstring>
#include <iostream>
int main() {
  char text[] =
      "That was yet unnamed warship was built in a Dalian shipyyard next to "
      "where the *Liaoning* is berthed and began a final sea trial in the "
      "Bohai Sea on Sunday, heading out to the Yellow Sea. Its design is based "
      "on the *Liaoning* but with a number of modifications.";
  char keyword[] = "Liaoning";
  int count = 0;
  char *positions[10];
  // ============= begin =============
  // 在text中找到keyword所有出现的位置,存放在positions数组中,并统计出现次数存放在count中
  // =============  end  =============
  std::cout << "count = " << count << std::endl;
  for (int i = 0; i < count; i++) {
    std::cout << "positions[" << i << "] = " << positions[i] << std::endl;
  }
  return 0;
}

总结

本节内容

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 20px
---
mindmap
字符串
  指针与数组
    数组作为指针
    指针作为数组
    指针与数组的互换性
  字符串
    字符数组
    字符串
    字符指针
  字符串函数
    strlen
    strcpy
    strcat
    strcmp
    strchr
    strstr

学习目标

  • 通过数组与指针的互换性更加深刻地理解地址
  • 掌握字符串、字符数组、字符指针的区别与联系
  • 使用字符串函数解决实际问题

课后作业

  • 实训(截止时间2025.04.02
    • C&C++指针实训
    • 综合练习—C&C++字符串
  • 预习
    • 教材6.2:多维数组
    • 教材7.7:动态内存分配

计算机程序设计