跳到主要内容

字符串与内存管理

字符串练习

str 中 substr 出现的次数

str = “hellollolloabcllollollo ”
substr: “llo” strlen(“llo”)

写函数测试,llo 在字符串中出现了多少次。使用 strstr() 函数。

#include <string.h>
char *strstr(const char *haystack, const char *needle);
char *strstr(const char *str, const char *substr);
// 参1:原串
// 参2:子串
// 返回值:
// 成功:返回子串在原串中的位置(地址值)
// 失败:NULL
// 封装函数,统计 str 字符串中,substr 出现的次数
int substr_times(char* str, char* substr)
{
int count = 0; // 定义变量统计 substr 出现的次数。
char *p = strstr(str, substr); // ---> "llolloabclloxyzllollo";
// 循环的截取 str 串,判断剩余 str 串中是否包含 substr
while (p != NULL) // while (p)
{
count++;
p += strlen(substr); // “lloabclloxyzllollo";
p = strstr(p, substr);
}
return count;
}
// 在 str 字符串中,找子串 substr 出现的次数
int main(void)
{
char str[] = "hell9oqqqlloabclloxyzlmlollo";
char substr[] = "llo";
int ret = substr_times(str, substr);
printf("%s 串中,%s 子串 出现 %d 次\n", str, substr, ret);
system("pause");
return EXIT_SUCCESS;
}

求字符串非空格元素个数

统计 "ni chou sha ? chou ni za di ! zai chou yi ge shi shi" 中,除空格外字符的个数。

// 统计非空格数
int no_space_num(char* str)
{
int count = 0;
// 指针方式实现。
char* p = str;
while (*p)
{
if (*p != ' ')
{
count++;
}
p++;
}
return count;
}
int main(void)
{
char str[] = "hello ni hao ma world?";
int ret = no_space_num(str);
printf("ret = %d\n", ret);
system("pause");
return EXIT_SUCCESS;
}

字符串逆置

“hello” ---> “olleh”
“word” ---> “drow”

参考“数组逆置”实现(day05)。

// h e l l o
// 字符串的逆序
void str_inverse(char* str)
{
char* start = str; // 记录首个元素的地址
char* end = str + strlen(str) - 1; // 记录最后一个元素的地址
// 循环交换字符串首尾元素
while (start < end)
{
char tmp = *start; // 三杯水交换字符元素。
*start = *end;
*end = tmp;
start++; // 首元素指针后移
end--; // 尾元素指针前移
}
}
int main(void)
{
char str[] = "this is a test";
str_inverse(str);
printf("%s\n", str);
system("pause");
return EXIT_SUCCESS;
}

判断字符串是回文

abcba --- 是回文
amkilolikma --- 是回文
abccba --- 是回文
abcdba --- 不是回文
// 判断字符串是否是回文
int str_is_abcba(char* str)
{
char* start = str;
char* end = str + strlen(str) - 1;
while (start < end)
{
if (*start != *end)
{
return 0; // 不是回文
}
start++;
end--;
}
return 1;
}
int main(void)
{
char str[] = "abcmnmcba";
int ret = str_is_abcba(str);
if (ret == 1) // 是回文
{
printf("%s 是回文!\n", str);
}
else if (ret == 0)
{
printf("%s 不是回文!\n", str);
}
system("pause");
return EXIT_SUCCESS;
}

字符串处理函数

全部是标准 C 库函数,使用头文件 #include <string.h>

字符串拷贝

strcpy

将 src 的内容,拷贝给 dest,返回 dest。dest 空间要足够大。

strcpy 函数不检查 dest 是否足够大 —— 【不安全函数】。函数调用结束,返回值和 dest 结果一致。

char *strcpy(char *dest, const char *src); // src: source dest: dst
// 字符串拷贝
int main(void)
{
char str[] = "you will be die if you copy me!";
char dst[100] = { 0 };
char *p = strcpy(dst, str);
printf("dest = %s\n", dst);
printf("p = %s\n", p);
system("pause");
return EXIT_SUCCESS;
}

strncpy

将 src 的内容拷贝给 dest,只拷贝 n 个字节。dest 空间要足够大,通常 n 与 dest 的空间大小一致。

特性:

  • n > src:只拷贝 src 大小。
  • n < src:只拷贝 n 个字节,不会自动添加 \0
char *strncpy(char *dest, const char *src, size_t n); // 安全
// 测试
int main(void)
{
char str[] = "hello world";
char dst[100] = { 0 };
char* p = strncpy(dst, str, sizeof(dst));
//for (size_t i = 0; i < 10; i++)
//{
// printf("%c\n", p[i]);
//}
printf("%s\n", p);
system("pause");
return EXIT_SUCCESS;
}

字符串拼接

strcat

将 src 中内容,拼接到 dest 后,返回拼接成功的字符串。需要保证 dest 空间足够大。函数调用结束后,dest 和返回值结果相同。

char *strcat(char *dest, const char *src);
int main(void)
{
char str[] = "hello world";
char dst[100] = "haha hoho xixi";
char *p = strcat(dst, str);
printf("p = %s\n", p);
printf("dst = %s\n", dst);
system("pause");
return EXIT_SUCCESS;
}

strncat

将 src 中前 n 个字符,拼接到 dest 后,返回拼接成功的字符串。需要保证 dest 空间足够大。函数调用结束后,dest 和返回值结果相同。

char *strncat(char *dest, const char *src, size_t n);
// 字符串拼接 strncat
int main(void)
{
char str[] = "hello world";
char dst[100] = "haha hoho xixi";
char* p = strncat(dst, str, 7);
printf("p = %s\n", p);
printf("dst = %s\n", dst);
system("pause");
return EXIT_SUCCESS;
}

字符串比较

字符比较可以使用 > < <= >= == !=,字符串比较不允许使用。

strcmp

比较 s1 和 s2 两个字符串,如果相等返回 0;如果不相等,进一步比较 s1 和 s2 对应位上的 ASCII 码值。

  • s1 > s2 返回 1
  • s1 < s2 返回 -1
int strcmp(const char *s1, const char *s2);
int main(void)
{
char s1[] = "helloz";
char s2[] = "helloaworld";
printf("ret = %d\n", strcmp(s1, s2)); // 不比较 ASCII 的 和
system("pause");
return EXIT_SUCCESS;
}

strncmp

比较 s1 和 s2 两个字符串的前 n 个字符,如果相等返回 0;如果不相等,进一步比较 s1 和 s2 对应位上的 ASCII 码值(不比较 ASCII 的和)。

int strncmp(const char *s1, const char *s2, size_t n);
char s1[] = "helloz";
char s2[] = "helloaworld";
printf("ret = %d\n", strncmp(s1, s2, 5)); // --- 0

字符串格式化输入、输出

(s 代表 string。)

sprintf

对应 printf 记忆。作用是将原来输出到屏幕的“格式化字符串”,写到参1 的 str 中。

int sprintf(char *str, const char *format, ...);
// ... 代表 这是一个参数可变的函数。
// printf("%d + %d = %d\n", 10, 24, 10+24);
char str[1024] = {0}; // 保证空间足够大
sprintf(str, "%d + %d = %d\n", 10, 24, 10 + 24); // 写到 str 中,不打印屏幕
puts(str);
printf("---%s", str);

sscanf

对应 scanf 记忆。作用是将原来从键盘获取到的“格式化字符串”,从参1 的 str 中获取。

int sscanf(const char *str, const char *format, ...);
int a, b, c;
// scanf("%d+%d=%d", &a, &b, &c); // 从键盘 stdin 读取。
char str[] = "10+20=30"; // 提供给 sscanf 参1 使用。
int ret = sscanf(str, "%d+%d=%d", &a, &b, &c);
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("c = %d\n", c);

字符串查找字符、子串

strchr

在字符串 s 中,找字符 c 出现的位置,返回字符在字符串中的地址。

char *strchr(const char *s, int c);

strrchr

r:right。自右向左,在字符串 s 中,找字符 c 出现的位置,返回字符在字符串中的地址。

char *strrchr(const char *s, int c);

strstr

在字符串 str 中,找寻子串 substr 第一次出现的位置,返回地址。

char *strstr(const char *str, const char *substr);

字符串分割

strtok

按照(参2)既定的分割符,来拆分字符串。例如 www.baidu.com 按 “.” 拆分。

结论:

  • strtok() 函数直接在原串上对字符串分割,不能分割字符串常量(如 char *str = "hello";)。
  • strtok() 函数调用结束,会将分割符替换成 \0
  • 第一次用 strtok 拆分,参1 传待拆分的原串;第 1+ 次拆分,参1 传 NULL。
char *strtok(char *str, const char *delim);
// 参1:待拆分字符串
// 参2:分割符组成的字符串。 strtok("www.baidu.com", "."); // 写成 '.' 错误!
// 测试1:
int main(void)
{
char str[] = "www.itcast.cn"; // ---》 "www\0itcast.cn";
char *p = strtok(str, "."); // strtok 调用完成,会将 分割符用 \0 替换。
printf("p = %s\n", p);
// 调用一次 strtok 分割之后,再去打印原串 str
for (size_t i = 0; i < 13; i++)
{
//printf("%c\n", str[i]);
printf("%d\n", str[i]); // 打印每个字符的 ASCII 码
}
system("pause");
return EXIT_SUCCESS;
}

练习:拆分字符串 “www.itcast.cn$This is a test$for strtok”,按分割符 “. $” 拆分……

字符串转换

a 代表字符串 string。将字符串转整数、小数、长整数、长长整型。

使用这类函数进行转换时,要求原串必须是可转换的字符串。错误使用:“abc123”“xyac123”“1245dke89” 不能正确转换。

atoi / atof / atol / atoll:

#include <stdlib.h>
int atoi(const char *nptr);
double atof(const char *nptr);
long atol(const char *nptr);
long long atoll(const char *nptr);

示例:

char str1[] = "12abc3456";
int num = atoi(str1);
printf("num = %d\n", num);

char str2[] = "3.14";
double num2 = atof(str2);
printf("num2 = %lf\n", num2);

char str3[] = "34568490354";
long long num3 = atoll(str3);
printf("num3 = %lld\n", num3);

内存管理

局部变量

概念:定义在函数内部的变量。

作用域:从定义位置开始,到包裹该变量的第一个右大括号结束(函数作用域、块作用域)。

void test(void)
{
{
int m = 10; // 局部变量。 --- 块作用域。出了 } 不能使用。
}
printf("---m = %d\n", m);
//int i = 0;
for (size_t i = 0; i < 10; i++)
{
printf("m = %d\n", m);
}
//printf("i = %d\n", i);
}

全局变量

概念:定义在函数外部的变量。

作用域:从定义位置开始,默认到本文件内部。其他文件如果想使用,可以通过“声明”的方式,将作用域导出。

static 变量

static 全局变量

  • 定义语法:在全局变量定义之前,添加 static 关键字。如 static int a = 10;
  • 作用域:被限制在本文件内部,不允许通过“声明”方式导出作用域(与 Java 不同)。

static 局部变量

  • 定义语法:在局部变量定义之前,添加 static 关键字。
  • 作用域:从定义位置开始,到包裹该变量的第一个右大括号结束。
  • 特性:静态局部变量只定义一次,相当于在全局位置定义。通常用来做计数器。

static 函数

全局函数:就是“函数”。定义语法:函数原型 + 函数体。

static 函数:定义语法为 static 函数原型 + 函数体。特性:static 函数只能在本文件内使用,其他文件即使声明也无法使用。

// static 关键字能将 test09 函数限制在本文件内。外部文件不能访问
static void test09(void)
{
for (size_t i = 0; i < 5; i++)
{
printf("i = %d\n", i);
}
}

A.c 文件中有上述代码。B.c 文件中调用上述函数,由于 static,B.c 不能使用 test09 函数,会报错:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
// void test09(void); // 声明函数
extern void test09(void); // 声明函数
int main(void)
{
test09();
system("pause");
return EXIT_SUCCESS;
}

生命周期

助记:

  • 生命周期:出生 —— 死亡 80 岁,生命周期 80 年。
  • 作用域:班长管全班,村长管全村,县长管全县。
类别生命周期说明
局部变量定义位置开始,函数调用结束(存储在栈 stack 上)函数被调用期间
全局变量从程序启动开始(早于 main()),程序终止结束程序执行期间
static 局部变量从程序启动开始,程序终止结束(定义在全局位置)程序执行期间
static 全局变量从程序启动开始,程序终止结束程序执行期间
全局函数从程序启动开始,程序终止结束程序执行期间
static 函数从程序启动开始,程序终止结束程序执行期间

命名冲突

如果全局变量和局部变量命名冲突,采用就近原则。强烈不推荐!

内存 4 区模型

  1. 代码段:.text 段,存储程序源码(二进制形式)。
  2. 数据段:只读数据段 .rodata、初始化数据段 .data、未初始化数据段 .bss。
  3. stack(栈):在其之上开辟栈帧(较小:Windows 1M10M,Linux 8M16M)。存储特性:后进先出 FILO(LIFO)。
  4. heap(堆):给用户自定义提供空间(较大:约 1.3G+)。

Heap 堆空间

开辟 / 释放 heap 空间

在 heap 上开辟空间:

#include <stdlib.h>
void *malloc(size_t size); // 向 系统申请内存空间,在 heap 上。单位:字节。
// 参数:申请空间的大小。
// 返回值:
// 成功:heap 上内存空间的首地址。
// 失败:NULL
// 申请成功的内存,通常拿来当成 “数组” 使用

释放 heap 申请的空间:

void free(void *ptr);
// 参:就是 malloc 函数的返回值。

示例:

int main(void)
{
// int arr[10];
// 申请能存储 10 个 int 数的空间。40 字节。
int *p = (int *)malloc(sizeof(int)*10); // 强转的目的,方便阅读代码。
if (p == NULL)
{
printf("malloc error\n");
return -1; // 退出程序。-1,非正常结束。
}
// 写 - 数据到 malloc 申请的空间
for (size_t i = 0; i < 10; i++)
{
p[i] = i + 10; // 存 数据到 malloc 申请的空间中。
}
// 从 malloc 申请的空间中,读数据。
for (size_t i = 0; i < 10; i++)
{
printf("%d ", *(p+i));
}
printf("\n");
// 释放 malloc 申请的空间。
free(p);
system("pause");
return EXIT_SUCCESS;
}

使用 heap 空间注意事项

  1. 申请的 heap 堆内存空间连续,当成“数组”使用。
  2. free 后的空间不会立即失效,通常将 free 后的地址置为 NULL。
  3. free 的地址必须是 malloc 函数返回的地址,否则报错!
  4. 如果 malloc 后的地址一定会变化,通常使用临时变量 tmp 保存。

二级指针对应的 heap 空间

📷(此处原文为配图,略)

内存操作函数

memset

#include <string.h>
void *memset(void *s, int c, size_t n);

memcpy

void *memcpy(void *dest, const void *src, size_t n);

memmove

void *memmove(void *dest, const void *src, size_t n);

memcmp

int memcmp(const void *s1, const void *s2, size_t n);

内存常见问题

📷(此处原文为配图,略)