23.1 预备知识
strcpy 和 strncpy
把一个字符串拷贝给另一个字符串。
strcpy 拷贝时包括结尾的 \0。
确保读写不会越界是调用者的责任。
凡是有指针参数的 C 标准库函数基本上都会要求 str 和 dest 所指向的空间不能重叠。
strncpy 参数 n 指定最多从 src 中拷贝 n 个字节到 dest 中:
- 提前遇到 src 的 \0 则提前结束,后续不足 n 的部分,全用 \0 填充;
- 但若 n 比 src 小,只拷贝了 src 的一部分,则要自己负责 dest 能以 \0 结尾,通常是拷贝完后,手动替换最后一个字符为 \0,如
buf[sizeof(buf)-1] = '\0'; - 至于 n 比 src 还要大,则读 src 会越界,这就是调用者的锅了。
总之,strncpy 会保证向 dest 中写满 n 个字符,调用时通常让 n 的值等于 dest 所指向的内存空间的大小(保证不会写越界)。
char buf[10];strncpy(buf, "hello world", sizeof(buf));buf[sizeof(buf)-1] = '\0';
strcpy 比 strncpy 更不安全,使用 strcpy,当 src 超过 dest 长度时就会写越界。
写越界可能覆盖了保存在栈帧上的返回地址,导致函数返回时跳转到非法地址而出错。
像 buf 这种由调用者分配并传给 strcpy 函数访问的一段内存通常称为缓冲区(Buffer)。
缓冲区写越界的错误称为缓冲区溢出。
该 bug 可能被恶意利用,使函数返回时跳转到事先设计好的地址,执行指令,甚至启动一个 shell。
实现一个 strcpy():
#include <stdio.h>/*** 实现一个 strcpy 函数,将 src 字符串复制到 dst 字符串中,并返回 dst。*/char *my_strcpy(char *dest, const char *src){for (int i; i < sizeof(src); i++)dest[i] = src[i];return dest;}/* linux的实现 参照:http://www.chinaunix.net/old_jh/23/25356.html */char *strcpy(char *strDest, const char *strSrc){if ((strDest == NULL) || (strSrc == NULL)) //[1]// throw "Invalid argument(s)";printf("wrong"); //[2]char *strDestCopy = strDest; //[3]while ((*strDest++ = *strSrc++) != '\0'); //[4]printf("ok"); //[2]return strDestCopy;}int main(void){// int i = 10;char buf[10] = {'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c'};strcpy(buf, "Hello");// printf("%s\n", buf);// printf("%d\n", sizeof(buf));printf("[%c][%c][%c]\n", buf[4], buf[5], buf[6]);printf("--------[%c]--------\n", buf[8]);}
实现一个压缩字符串空白的函数 shrink_space:
/*** 编一个函数,输入一个字符串,要求返回一个新字符串,把其中所有的一个或多个连续空白字符都压缩成一个空格。* 空白字符包括:空格、'\t'、'\n'、'\r'。** @file 1-2.c* @date 2022-05-12*/#include <stdio.h>#include <stddef.h>int is_space(char c){return c == ' ' || c == '\t' || c == '\n' || c == '\r';}char *shrink_space(char *dest, const char *src, size_t n){size_t i;char *result = dest;for (i = 0; i < n && src[i] != '\0'; i++){if (!is_space(src[i])){*dest++ = src[i];}else if (!is_space(src[i - 1])){*dest++ = ' ';}}for (; i < n; i++){*dest++ = '\0';}return result;}int main(void){char buf[20];shrink_space(buf, " Abc \t\n def \r gh. ", sizeof(buf));printf("[%s]\n", buf);return 0;}
malloc 和 free
C 标准库函数 malloc 可以在堆空间动态分配内存,底层通过 brk 系统(抬高 break)调用向操作系统申请内存。动态分配的内存用完之后用 free 释放(准确说是归还给了 malloc),下次调用 malloc 时这块内存可再次分配出来。
#include <stdlib.h>
void *malloc(size_t size); // size 是要分配的字节数
// 成功返回所分配内存空间的首地址,出错返回 NULL 野指针
void free(void *ptr); // ptr 是之前用 malloc 申请的内存块首地址
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct
{
int number;
char *msg;
} unit_t;
int main(void)
{
unit_t *p = malloc(sizeof(unit_t));
if (p == NULL)
{
printf("out of memory\n");
exit(1);
}
p->number = 3;
p->msg = malloc(20);
strcpy(p->msg, "hello world");
printf("number: %d\nmsg: %s\n", p->number, p->msg);
free(p->msg);
free(p);
p = NULL;
return 0;
}
分配完内存不释放,会慢慢耗尽系统内存,称为内存泄漏(Memory Leak),大量内存泄漏会导致物理内存紧缺,换页频繁,拖慢当前进程甚至是整个系统。
malloc(0) 是合法的,返回一个非 NULL 指针,也可传给 free 释放,但不能通过该指针访问内存。
free(NULL) 也合法,不做任何事。
但 free 一个野指针就不合法了,如分配了 p 再连续两次 free(p),第二次就会报运行时错误。
基于环形链表 的 malloc 和 free 简单实现
实际 libc 的实现要复杂很多。
Break 以上不属于当前进程的地址空间,需要通过 brk 系统调用向内核申请,由内核分配物理内存映射到进程地址空间,并抬高 Break。
调用 malloc 申请的内存块,就属于用户程序了,free 释放后成为空闲块后才继续归 malloc 管理。
环形链表只串联空闲块。
19.5 链接详解-虚拟内存管理提到过进程地址空间:
23.2 传入参数与传出参数
strcpy 的 src 参数就是传入参数,dest 参数就是传出参数。有些函数的指针参数同时充当这两种角色,称为 Value-result 参数。
很多系统函数对于指针参数时 NULL 的情况有特殊规定,如传入的是 NULL 表示缺省值,或表示不做特别处理。如传出的是 NULL 表示调用者不需要传出值等。
23.3 两层指针的参数
两层指针也是指针,同样可以表示传入参数、传出参数或者 Value-result 参数,只不过该参数所指的内存空间应该解释成指针变量。
两层指针作为传出参数还有一个特别用法,可以在函数中分配内存,调用者通过传出参数取得指向该内存的指针。
23.4 返回值是指针的情况
实现者返回一个指针,调用者将返回值保存下来以备后用。
所以通过调用函数来分配内存有两种:
- 上一节的通过两层指针的传出参数函数,间接操作外部指针变量本身的内存空间,实现内存分配。
- 本节的直接返回分配后的指针,调用者只需将这个返回的指针重新赋值给外部指针变量即可。
23.5 回调函数
参数是一个函数指针,调用者将函数地址传递给实现者,让实现者去调用它,这称为回调函数,如 qsort(3) 和 bsearch(3) 。
回调函数接口示例:void func(void (*f)(void *), void *p);
/* para_callback.h */
#ifndef PARA_CALLBACK_H
#define PARA_CALLBACK_H
typedef void (*callback_t)(void *);
void repeat_three_times(callback_t, void *);
#endif
/* para_callback.c */
#include "para_callback.h"
void repeat_three_times(callback_t cb, void *para)
{
cb(para);
cb(para);
cb(para);
}
/* main.c */
#include <stdio.h>
#include "para_callback.h"
static void say_hello(void *str)
{
printf("Hello %s\n", (const char *)str);
}
int main(void)
{
repeat_three_times(say_hello, "World");
return 0;
}
回调函数的一个经典应用就是实现类似 C++ 的泛型算法,书中实现了一个通用的泛型 max 函数,可以在任意一组对象中找出最大值,调用者还要提供一个做比较的回调函数:
/* generics.h */
#ifndef GENERICS_H
#define GENERICS_H
#include <stddef.h>
typedef int (*cmp_t)(const void *, const void *);
/* 对象数组基地址,共有多少个对象,每个对象大小,比较函数 */
void *max(const void *base, size_t nmemb, size_t size, cmp_t cmp);
#endif
/* generics.c */
#include "generics.h"
void *max(const void *base, size_t nmemb, size_t size, cmp_t cmp)
{
const char *_base = base;
const char *temp = _base;
size_t i;
for(i = 1; i < nmemb; i++)
if(cmp(temp, _base + size * i) < 0)
temp = _base + size * i;
return (void *)temp;
}
/* main.c */
#include <stdio.h>
#include "generics.h"
typedef struct {
const char *name;
int score;
} student_t;
int cmp_student(const void *a, const void *b)
{
if(((student_t *)a)->score > ((student_t *)b)->score)
return 1;
else if(((student_t *)a)->score == ((student_t *)b)->score)
return 0;
else
return -1;
}
int main(void)
{
student_t list[4] = {{"Tom", 68}, {"Jerry", 72},
{"Moby", 60}, {"Kirby", 89}};
student_t *pmax = max(list, sizeof(list)/sizeof(student_t), sizeof(student_t), cmp_student);
printf("%s gets the highest score %d\n", pmax->name, pmax->score);
return 0;
}
以上回调函数都是被同步执行,还有异步执行的回调。
调用者将回调函数传给实现者,实现者注册(记住)该回调函数,当某个事件发生时,实现者再调用先前注册(存储)的函数。
比如 sigaction(2) 注册一个信号函数,当信号产生时由操作系统调用该信号函数。
再比如 pthread_create(3) 注册一个线程函数,当发生调度时操作系统切换到新注册的线程函数中运行。
GUI 编程中为按钮注册回调函数,当用户点击按钮时被调用。
注册回调函数的代码框架:
/* registry.h */
#ifndef REGISTRY_H
#define REGISTRY_H
typedef void (*registry_t)(void);
extern void register_func(registry_t);
#endif
/* registry.c */
#include <unistd.h>
#include "registry.h"
static registry_t func;
void register_func(registry_t f)
{
func = f;
}
static void on_some_event(void)
{
...
func();
...
}
参数和返回值都可以是函数指针,因此可以 func()() 调用,但 C 语言中很少见返回函数指针,更多是在函数式编程语言中比较常见,如 LISP。基本思想是把函数也当做一种数据来操作,可以输入、输出和参与运算,操作函数的函数称为高阶函数(High-order Function)。
实现 C 标准库 qsort(3) 函数
#include "qsort.h"
// #include <stdlib.h>
#include <stdio.h>
// 参考文章 https://blog.csdn.net/Junadhkjashka/article/details/121695781
// 一个字节一个字节的拷贝
void swap(char *bub1, char *bub2, int w)
{
int i = 0;
//这里所接收的都是char*类型的指针,所以每次交换只能交换一个字节,如果想要完整的交换原本的两个元素,则需要接收原本元素所占字节大小
for (i = 0; i < w; i++)
{
char temp = *bub1;
*bub1 = *bub2;
*bub2 = temp;
bub1++;
bub2++;
}
}
void qsort(void *base, size_t nmemb, size_t size, compar cmp)
{
void *_base = base;
char *current;
char *next;
// 不好意思,冒泡排序~
for (size_t i = 0; i < nmemb - 1; i++)
for (size_t j = i + 1; j < nmemb; j++)
{
current = _base + size * i;
next = _base + size * j;
if (cmp(current, next) > 0)
{
swap(current, next, size);
// 下面这样也行
// for (size_t k = 0; k < size; k++)
// {
// char temp = *(current + k);
// *(current + k) = *(next + k);
// *(next + k) = temp;
// }
}
}
}
int compar_cb(const void *a, const void *b)
{
return *(int *)a - *(int *)b;
}
int main(void)
{
int arr[5] = {5, 4, 3, 2, 1};
qsort(arr, 5, sizeof(int), compar_cb);
for (int i = 0; i < 5; i++)
printf("%d\n", arr[i]);
return 0;
}
实现 C 标准库 bsearch(3) 函数
23.6 可变参数
要处理可变参数,需要用到 C 标准库的 stdarg.h 中的 va_list 类型和 va_start、va_arg、va_end 宏。
stdarg.h 取参数的原理,就是不断修改指针指向,然后用 *p 间接寻址运算符从函数的栈帧上取值即可,其中涉及到精妙的位运算就先不细究了。
用可变参数实现简单的 printf 函数:
#include <stdio.h>
#include <stdarg.h>
void myprintf(const char *format, ...)
{
va_list ap;
char c;
va_start(ap, format);
while (c = *format++)
{
switch (c)
{
case 'c':
{
/* char is promoted to int when passed through '...' */
char ch = va_arg(ap, int);
putchar(ch);
break;
}
case 's':
{
char *p = va_arg(ap, char *);
fputs(p, stdout);
break;
}
default:
putchar(c);
}
}
va_end(ap);
}
int main(void)
{
myprintf("c\ts\n", '1', "hello");
return 0;
}
照理说,printf 实现者只管按格式化字符串的描述从栈上取参数即可,调用者传入的参数类型和个数不正确,是调用者的事情。
但另一种可以确定可变参数的个数的办法,就是在参数列表的末尾按约定传一个 NULL 哨兵(Sentinel),如 execl(3) 函数。具体用法用到再说。
