文章目录
(八) 指针
0.概念
①计算机最小寻址单位:字节
②变量的地址:变量首字节的地址
③指针:就是地址
④指针变量:存储地址值的变量
⑤野指针:不知道指向哪个对象的指针
⑥空指针:不指向任何对象的指针
⑦常量指针和指针常量
⑧传入参数和传出参数
⑨通用指针类型 void*
1.指针基础
(1)指针的声明
1.int *p 或 int* p
*说明了p是指针
2.变量名:p,类型:int*
3.注意事项:声明指针变量时,需要指定它指向对象的类型
int是指向对象的类型:①说明对象所占内存大小 ②如何解释那片内存空间 (说明了对象的类型)
(2)指针的两个基本操作
①取地址运算符 &

②解引用运算符 *

0.示例
i:直接访问,逻辑上访问内存一次
*p:间接访问,逻辑上访问内存两次
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void) {
int i = 1;
int* p = &i;
printf("*p = %d\n", *p);
*p = 2; //*p 是 i 的别名,有读写权限
printf("i = %d\n", i);
return 0;
}
(3)野指针
①野指针
(1)野指针:不知道指向哪个对象(哪块数据)
(2)对野指针进行解引用,是未定义行为
(3)野指针的两种表现形式
int* p; //1.不初始化
int* q =0xABCD; //2.用一个整数赋值
(4)正确地给指针变量赋值的两种方式
int* p = &i; //1.
int* q = p; //2.
p = NULL; //2.

②空指针
(1)空指针(NULL):不指向任何对象的指针,不指向任何有效的内存地址
(2)不能对空指针进行解引用
③指针变量的赋值 vs 指针变量指向对象的赋值
①p = q
②*p = *q
(4)指针的应用
①指针作为参数进行传递
好处:在被调函数中可以修改主调函数中变量的值,解引用
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void swap(int* p, int* q) {
int temp = *p;
*p = *q;
*q = temp;
}
int main(void) {
int a = 3, b = 4;
printf("a = %d, b = %d\n", a, b);
swap(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}

②指针作为返回值
教训:不要返回指向当前栈帧区域的指针 (因为返回以后,该栈帧就出栈了,变量被销毁。超出作用域后,局部变量的指针就成了悬空指针)

③拓展:栈帧
栈帧 esp、ebp
栈帧里存储的是:函数调用相关的信息:形参、局部变量、返回地址
(5)常量指针、指针常量
const本质:限制变量的写权限
这里const是限制指针变量p的写权限
1.不加const,正常情况

2.const int* p (pointer to const)
对内存1有写权限,但对内存2没有写权限。
即可以修改指针的指向,但不能通过 *p 对所指变量的值进行修改。(但变量i自身可修改)
3.int* const p (const pointer)
对内存1没有写权限,但对内存2有写权限。
即不能修改指针的指向,但可以通过 *p 修改所指对象的值

4.const int* const p (const pointer to const)
对内存1和内存2都没有写权限。
即不能修改指针的指向、不能通过 *p 改写变量的值

(6)传入参数、传出参数
函数的声明:
1.指针类型的参数:
①char *buf:传出参数,几乎一定会修改buf指向对象
②const char * buf:传入参数,不会修改指针指向的对象
2.指针类型的返回值:
①栈
②静态区(数据段、代码段)
③堆:谁调用,谁free
1.传入参数:const int* p
在函数里面,不能够通过指针变量修改指针指向的对象

2.传出参数:int* p
在被调函数中可通过指针变量修改主调函数中指向的对象的值,可替代返回值来用 (C语言返回值只能返回一个值,但指针修改可修改多个值,即通过传出参数可代替多个返回值)



传入参数和传出参数,指的都是指针变量
2.指针与数组
(1)指针的算数运算:加法、减法、比较
1.指针的加法:
①指针 + 整数:指针向右偏移几个单位
p = p+3; //指针向右偏移3个单位
2.指针的减法:
①指针减去整数n,代表指针向左偏移n个单位
p = p-3; //指针向左偏移3个单位
②两个指针相减,结果为一个整数,相隔几个单位
int n = p-q;
3.指针的比较运算

(2)指针和数组的关系
概念:
数组是一片连续的内存空间,并被划分为一个个大小相等的小空间
指针与另一个对象进行关联
①用指针处理数组:指针代替索引
指针处理数组(早期C语言)
现代的编译器会把for顺序处理在编译层面翻译成第一种写法,避免了乘法运算
②数组可以退化为指向数组第一个元素的指针
在必要的时候,数组可以退化为指向数组第一个元素的指针
退化:&arr[0] 就可以写为 arr
数组会退化为指针的情景:
①数组作为参数传递: fun(arr)
②数组给指针变量赋值时:int* p = arr (数组在赋值表达式的右边,即数组进行赋值运算时作为右值)
③数组参与算术运算: arr+3

③指针也支持取下标运算
p[i] 等价于 *(p+i)
p[i]等价于*(p+i),等价于*(i+p)等价于i[p]。
故,防御性编程:i[arr]
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void) {
int arr[5] = { 0,1,2,3,4 };
//1.指针也支持取下标运算: p[i]
int* p = arr;
for (int i = 0; i < 5; ++i) {
printf("%d ", p[i]);
}
printf("\n");
//2.防御性编程: i[arr]
for(int i = 0; i < 5; ++i){
//printf("%d ", arr[i]); //arr[i] 等价于 *(arr+i)
printf("%d ", i[arr]); //i[arr] 等价于 *(i+arr)
}
printf("\n");
return 0;
}
(3)*与++的组合
| 表达式 | 表达式的值 | 副作用 |
|---|---|---|
① *p++ 或 *(p++) | *p | p自增 |
② (*p)++ | *p | *p自增 |
③ *++p 或 *(++p) | *(p+1) | p自增 |
④ ++*p 或 ++(*p) | (*p)+1 | *p自增 |
①*p++ 或 *(p++) :表达式值为 *p,副作用是 p自增 (最常见)
②(*p)++ : 表达式值为 *p,副作用是 *p自增
③*++p 或 *(++p):表达式值为 *(p+1),副作用是p自增
④++*p 或 ++(*p):表达式值为 (*p)+1,副作用是 *p自增
最常用的 *p++,见下文字符串复制

(4)指针支持的操作
①解引用 *:通过指针变量,获取它指向的对象
②算数运算:加整数、减整数、减指针、自增、自减、比较运算(==、!=、>、>=、<、<=)
③取下标 []
3.指针的高级应用
(1)动态内存分配
1.为什么要在堆上分配空间、为什么需要动态内存分配?(why)
①栈帧的大小是在编译期间确定的,栈空间不能存放动态大小的数据,如vector
②栈空间比较小,主线程8MB,其他线程2MB。所以栈上不能存很大的数据。
③每个线程都有自己的栈,多线程共享的数据,最好不要存在栈上,应该存在堆上
①内存分配函数:malloc、calloc、realloc
1.如何申请堆空间? (How)
答:使用内存分配函数
2.头文件:#include <stdlib.h>

(1)malloc
malloc:memory allocate,内存分配函数
分配连续内存的大小
int* p = malloc(sizeof(int) * 100);
(2)calloc
calloc 会初始化分配的内存块,使得所有字节都被设置为零。
clear + allocate:申请空间,并清零(全部赋值为0)。(空指针NULL值也是0)
void* calloc(size_t num, size_t size);
- 参数
- num:要分配的元素个数。
- size:每个元素的大小(以字节为单位)。
- 返回值
- 成功:返回指向已分配内存块的指针。
- 失败:返回 NULL。
calloc为 num_elements 个大小为 element_size 的元素分配内存空间,并将所有的位初始化为 0。如果函数成功,它将返回一个指向已分配内存的指针。如果失败,它将返回 NULL。
int* p = calloc(个数, sizeof(类型));
(3)realloc
调整先前分配的内存块大小。如果重新分配内存大小成功,返回新内存块的指针,否则返回空指针。(旧内存块不会被释放)
缩容是直接截断。尽可能地原地扩容,扩充的内存是未初始化的
void* reallloc(void* ptr, size_t size);

②空指针 NULL、通用指针
1.NULL
2.void * p 通用指针
作用:C语言中,通用指针 void * 可以与其他任意类型的指针 相互转化。也即 void * 指向对象的类型还不确定,不能直接操作(解引用、自增、加法等)通用指针。
③动态分配数组:vector
1.动态数组的实现
typedef int E;
typedef struct{
E* elements; //指向堆空间的数组
int capacity; //容量
int size; //实际个数
} Vector;

2.跨文件编写程序
依赖接口,不要依赖具体的实现 (因为实现是变化的,接口一般是固定的、稳定的)
接口:*.h、Interface、抽象类
接口中存放:类型定义和API的声明
3.头文件
(1)头文件中存放:类型的定义、API的声明
API的声明,可以给用户使用的。
但实现时不希望用户直接使用的函数(实现),就不要放到头文件中
(2)两种头文件
" " 自己写的头文件:搜索路径:指定的路径(若找不到) -> 当前目录 (若找不到) -> 系统头文件包含目录
<> 搜索路径:系统头文件包含目录 /usr/include
(3)依赖关系图

(2)释放内存空间:free
1.垃圾(garbage):不可再被访问的内存块
内存泄漏(memory leak):程序中存在垃圾
2.垃圾回收器:
(1)①有垃圾回收器的语言:Java、Python、Go
②手动管理垃圾的语言:C、C++、rust
(2)①垃圾回收器的特点:减轻程序员的负担、但引入了不确定因素。清除垃圾时会stop the world,不适合写实时系统。[实时系统:在某个确定的时间内完成某个任务]
②没有垃圾回收器:
C:free
C++:delete、析构函数、智能指针、RAII
Rust:所有权机制
3.free的问题
void free(void *ptr);
悬空指针(野指针的一种)
对堆上内存释放一次后,p就变成了悬空指针
(1)double free
free两次
(2)use after free
free后再使用
(3)忘记free
造成内存泄露
结论:当堆上的数据不再使用时,应该有且只释放一次

(3)动态分配结构体
见链表
(4)二级指针:指向指针的指针
Q:传一级指针还是传二级指针?
A:想修改哪个变量,就传那个变量的地址(指针)
①想修改指针指向的对象,传一级指针
②想修改指针的指向 (修改指针变量的值),传二级指针
(5)函数指针:指向函数的指针
1.概念
(1)函数指针是指向函数的指针变量。函数指针保存函数的地址,是函数的入口地址。
(2)回调函数(callback)就是一个被作为参数传递的函数。在C语言中,回调函数只能使用函数指针实现
C语言中,函数名就是函数的入口地址。
C语言中,数组名也是数组的入口地址。
在C语言中,“入口地址”通常是指指针,它代表了函数或数组在内存中的位置。这个地址是指向函数代码或数组第一个元素的地址。
函数名就是一个函数指针。更准确地说,函数名在使用时会被隐式地转换为指向该函数的指针,这个指针可以被赋值给函数指针变量,用于调用函数或传递给其他函数。
在 C 语言中,函数名(如 main) 和 函数名取地址(如&main)在表达式中是等价的,都会产生函数的入口地址。
printf("%p\n", main);
printf("%p\n", &main);
2.定义函数指针的一般形式:声明函数指针变量
返回类型 (*指针名称)(参数类型);
ret_type (*pointer_name)(parameter_list);
(1)声明一个函数指针
//定义一个函数指针
int (*pfunc)(int,int);
//绑定
pfunc = add;
//调用
pfunc(6,10); //省略形式
(2)声明一类函数指针,起别名:typedef
typedef int(*funcptr)(int,int);
funcptr p1 = add;
(*p1)(7,8); //完整形式
(3)C++11 定义类型别名:using
typedef 原名 新名;
using 新名 = 原名;
typedef T * iterator;
using iterator = T *;
using pFunc = int(*)(int,int); //C++11的语法
pFunc p1 = add;
p1(1,2);
举例:
//1.返回值类型是int, 函数指针名是p, 参数类型是 (int,int)
int (*p)(int, int);
//2.定义一个函数指针operation,返回值类型是void,参数是 (int,int)
void (*operation)(int, int);
//3.返回值类型是void, 函数指针名是sighanler_t, 参数类型是int
void (*sighandler_t)(int); //声明了一个具体的函数指针变量 sighandler_t
//4.将指向接受一个int参数,并返回void的函数指针类型,起了一个别名sighandler_t
typedef void (*sighandler_t)(int); //定义了一个新的类型 sighandler_t
sighandler_t handler; // 使用typedef定义的sighandler_t类型来声明函数指针变量 handler
handler = my_handler; // 将函数指针变量指向 my_handler 函数
signal(SIGINT, handler); // 设置信号处理程序
区分:声明函数
int* p2(int, int);
3.初始化 / 赋值 (用函数指针钩中具体的函数):
int (*p1)(int, int) = foo; //省略形式
int (*p2)(int, int) = &foo; //完整形式
4.通过函数指针调用函数
p(a,b); //省略形式
(*p)(a,b); //完整形式
5.举例,自己写的demo
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void add(int a, int b) {
printf("a + b = %d\n", a + b);
}
void subtract(int a, int b) {
printf("a - b = %d\n", a - b);
}
void multiply(int a, int b) {
printf("a * b = %d\n", a * b);
}
void devide(int a, int b) {
printf("a / b = %d\n", a / b);
}
void mod(int a, int b) {
printf("a %% b = %d\n", a % b);
}
void err(int a, int b) {
fprintf(stderr, "undefined identifier.\n");
}
int main(void) {
void (*operation)(int, int); //定义一个函数指针(钩子函数),返回值类型是void,参数是 (int,int)
printf("please input a b.\n");
int a, b;
scanf("%d%d", &a, &b);
getchar(); //吃掉回车
printf("please input operator.\n");
char op;
scanf("%c", &op);
switch (op) {
case '+': operation = add; break; //将函数指针operation 指向 add 函数
case '-': operation = subtract; break; //将函数指针operation 指向 subtract 函数
case '*': operation = multiply; break; //将函数指针operation 指向 multiply 函数
case '/': operation = devide; break; //将函数指针operation 指向 devide 函数
case '%': operation = mod; break; //将函数指针operation 指向 mod 函数
default: operation = err; //将函数指针operation 指向 err 函数
}
operation(a, b);
return 0;
}
6.作用 / 应用场景
(1)函数式编程 (传递函数,返回函数)
C语言通过函数指针,支持函数式编程
好处:分解任务,解耦合
(2)编写非常通用的函数 (功能非常强大的函数),如qsort()
(3)函数指针调用的函数,称为钩子函数,如cmp( )
函数指针实现了:分解任务,解耦合。(将排序和比较分开了。若没有函数指针调用函数,则比较的逻辑要写死在qsort中,类型要固定,无法实现通用的功能)
(4)延迟调用的思想:先注册,延迟调用。即回调函数的注册,回调函数的执行。
①qsort的cmp函数
②进程终止:atexit()
③线程创建:pthread_creatre()的start_routine
④信号处理函数
⑤动态多态

②线程创建pthread_create()的start_routine函数
#include <pthread.h>
int pthread_create(pthread_t* tid, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *args);
截取的线程池代码:
void* threadFunc(void* arg){
//先获取到线程池
threadpool_t* threadpool = (threadpool_t*)arg;
while(1){ //当队列中有结点时, 获取任务结点。当队列中没有结点时,就阻塞等待
int peerfd = taskDequeue(&threadpool->queue);
if(peerfd > 0){
transferFile(peerfd); //传输文件
close(peerfd); //关闭连接
}else{
break; //退出子线程
}
}
return NULL;
}
void threadpoolStart(threadpool_t* threadpool){
if(threadpool){
for(int i = 0; i < threadpool->threadNum; i++){
//pthread_create()的第三个参数是函数指针,将函数指针start_routine指向threadFunc函数
int ret = pthread_create(&threadpool->pthreads[i], NULL, threadFunc, threadpool);
THREAD_ERROR_CHECK(ret, "pthread_create");
printf("sub thread %ld\n", threadpool->pthreads[i]);
}
}
}
(6)回调函数
回调函数是一种通过函数指针来调用的函数。
(九) 字符串
0.总纲
①C语言没有字符串类型!
②C语言中的字符串,依赖字符数组存在 (字符数组最后一个存\0,才是C字符串)
③C语言中的字符串,是一种逻辑类型
(1)C字符串的遍历
在C语言中,字符串是一系列以空字符(‘\0’)结尾的字符数组。可以通过迭代每个字符来遍历字符串,有以下两种常用的方式:
①数组下标
1.使用数组下标:
#include <stdio.h>
int main() {
char str[] = "Hello, World!";
for (int i = 0; str[i] != '\0'; ++i) {
printf("%c\n", str[i]);
}
return 0;
}
②指针
2.使用指针:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void) {
char str[] = "Hello";
char* p = str;
while (*p != '\0') {
printf("%c", *p);
p++;
}
return 0;
}
完整代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void) {
char str[] = "Hello,";
char* p = str;
while (*p != '\0') {
printf("%c", *p++);
};
char str2[] = "World!";
for (int i = 0; str2[i] != '\0'; i++) {
printf("%c", str2[i]);
}
return 0;
}
③注意
①C语言中的字符串,以空字符\0结尾!
②C语言中求字符串的长度,需要从头遍历,是O(n)的时间复杂度。
length是O(1)的时间复杂度
strlen是O(n)的时间复杂度
④遍历字符串的三个效率级别
1.最糟糕的写法:
for (int i = 0; i < strlen(str); i++) { ... }
因为strlen()本身就是O(n)的复杂度,又嵌套在for里,使得这个遍历字符串的时间复杂度达到了O(n²)
2.稍微好一些的写法:
int len = strlen(str);
for (int i = 0; i < len; i++) { ... }
3.比较好的写法:数组下标 + ‘\0’
for (int i = 0; str[i] != '\0'; i++) { ... }
4.最好的写法:指针操作字符串
char* p = str;
while (p){
...
p++;
}
1.字符串常量 (字符串字面值)
(1)概念
字符串常量(字符串字面值),表示双引号括起来的字符序列
(2)字符串字面值的三种书写方式
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void) {
//字符串字面值的三种书写方式
//1.最普通的书写方式
printf("I love xixi -- From peanut\n");
//2.换行 (但是换行后不忽略空白字符)
printf("I love xixi \
-- From peanut\n");
//3.字符串拼接
printf("I love xixi"
" -- From peinut\n");
return 0;
}
当两个或更多个字符串字面值相邻时 (仅用空白字符分割),编译器会把
它们合并成一个
举例:模拟 输出图形

//打印菱形
printf(" *\n"
" * *\n"
" * *\n"
"* *\n"
" * *\n"
" * *\n"
" *\n" );
传统的两层for循环,可读性差,性能低。
(3)内存模型
1.字符串字面值,存放在代码段,不可被修改。
代码段存放:指令、字符串字面值
2.C语言中字符串以\0结尾
void空类型 (没有值)
\0空字符 (C字符串结束标志)
NULL空指针 (不指向任何对象)
""空字符串
(4)字符串字面值支持的操作
常量数组支持的操作,字符串字面值都支持。(可以把字符串字面值看作是常量数组)
"字符串内容"可作为数组名,支持取下标运算
char* p = "ABC" + 1; //"ABC"是数组名,进行算术运算时退化为首元素的指针,+1就是向右偏移1个单位
printf("%c\n",*p); //输出B
//十六进制转换
char digit_to_hex(int dight){
return "0123456789ABCDEF"[dight];
}
2.字符串变量
(1)声明字符串变量并赋初始值
两种方式,第二种是第一种的语法糖
//声明字符串变量,并赋初始值
char str0[] = { 'H','e','l','l','o' }; //字符数组
char str1[] = { 'H','e','l','l','o','\0'}; //字符串: 数组的初始化式 { }
char str2[] = "Hello"; //字符串: 语法糖,"Hello"是数组初始化式的简写形式
建议:
①如果初始化字符数组,用数组的初始化式,{'H','e','l','l','o','\0'}
②如果初始化字符串,用双引号语法糖,"Hello"
char类型,0值就是空字符\0
例:
char s[10] = {'H','e','l','l','o','\0'}
①字符数组长度:10
②字符串长度:5
③字符串占用的空间:6
(2)字符数组 vs 字符指针
char str[] = "hello"; //"hello":数组的初始化式
char* p = "hello"; //"hello":字符串字面值

例题:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//字符数组、字符指针、字符串字面值
int main(void) {
char name[] = "Edward"; //字符数组
char* name2 = "Edward"; //字符指针
if (name == "Edward") {
printf("name == \"Edward\"\n");
}else {
printf("name != \"Edward\"\n");
}
if (name2 == "Edward") {
printf("name2 == \"Edward\"\n");
}else {
printf("name2 != \"Edward\"\n");
}
return 0;
}
(3)char ** :字符指针数组
3.读 / 写字符串
(1)读:使用scanf和gets读字符串
①scanf+ %s:读取一个单词
scanf("%s",str); //数组名退化为指针,就是地址。不需要加取地址运算符
char str[MAX_SIZE];
scanf("%s", str); //scanf + %s,会忽略前置空白字符,遇到空白字符停止
printf("%s\n", str);
(1)%s的匹配规则:忽略前置空白字符,读取字符填入字符数组,遇到空白字符结束。
(2)缺点:
①不能够存储空白字符
②不会检查数组越界 (读多了,超过了数组长度,数据覆盖了数组后面的内存空间)
②gets():从stdin中读取一整行数据,存入字符数组。并将’\n’替换为’\0’
char str[100];
gets(str);
printf("You entered: %s\n", str);
1.匹配规则
gets()不会忽略前置空白字符,一次读取一行,遇到空白字符不结束,直到遇到换行符\n才结束,并将’\n’替换为’\0’
2.缺点:不会检查数组越界。(若要读取的字符串长度超过了字符数组的长度,也会照样写内存,覆盖数组后面的内存的数据,造成数组越界)
③fgets()
fgets(str, sizeof(str), stdin); //str,数组长度,从哪里读入
注意事项:
①fgets()会检查数组越界 (对比第二个数值)
②会保存换行符’\n’,并在后面添加’\0’。以\n\0结尾
(2)写:使用printf和puts写字符串
①printf + %s
输出一个字符串。(从头输出到空字符结束,空字符\0标志着字符串的结束)
%.ps,精度p:最多输出p个字符
char str[] = "Hello world";
printf("%s\n", str);
printf("%.5s\n", str); //%.ps
②puts():输出字符串
①puts():输出一个字符串
②puts()效率高于printf,不用处理格式化输出
printf("%s\n",str);
puts(str); //两种写法等价,puts()会自动添加换行符。puts()效率更高
4.C字符串的操作:C语言字符串库
头文件 <string.h>
(1)strlen
strlen():求字符串的长度,不包含'\0'
①惯用法:遍历字符串 / 搜索字符串末尾
while(*p != '\0'){
p++;
}

②sizeof() 和 strlen() 的区别:
char str1[10] = "abc";
char str2[ ] = "abc";
printf("sizeof(str1) = %d\n", sizeof(str1)); //10
printf("sizeof(str2) = %d\n", sizeof(str2)); //4
printf("strlen(str1) = %d\n", strlen(str1)); //3
printf("strlen(str2) = %d\n", strlen(str2)); //3
(2)strcpy
#include <string.h>
char* strcpy(char* dest, const char* src);
char* strncpy(char* dest, const char* src, size_t n);
void* memcpy(void* dest, const void* src, size_t n);
①strcpy(s1, s2):字符串复制
1.函数原型:
char* strcpy(char* dest, const char* src);
2.参数:
①dest:目标字符串的指针,必须有足够的空间来容纳源字符串及其终止的空字符。
②src:源字符串的指针,必须是一个以空字符结尾的字符串。
3.返回值:
指向目标字符串 dest 的指针
4.特点:
复制包括终止空字符在内的整个字符串。
只能用于字符数组(字符串)。
不检查目标数组的大小,可能导致缓冲区溢出。
5.举例
strcpy(str, "Hello");
char dest[20];
char src[] = "Hello, World!";
strcpy(dest, src);
②strncpy(s1, s2, count)
1.函数原型
char* strncpy(char* dest, const char* src, size_t n);
2.参数
①dest:目标字符串的指针,必须有足够的空间来容纳源字符串及指定的字符数
②src:源字符串的指针,必须是一个以空字符结尾的字符串
③n:最多复制的字符数
3.特点
如果源字符串的长度小于 n,则在复制完源字符串后,在目标字符串中填充空字符(‘\0’),直到总共复制了 n 个字符。
如果源字符串的长度大于或等于 n,则仅复制前 n 个字符,不会自动添加终止空字符。
strncpy可以防止缓冲区溢出,但当超出des数组长度时,不会自动添加终止空字符。
4.举例
strncpy(str,"Hello world",MAXLINE-1);
str[MAXLINE-1] = '\0';
void test_strncpy() {
char dest[10];
// 示例1:源字符串长度小于指定长度
strncpy(dest, "Hello", sizeof(dest));
cout << "Example 1: " << dest << endl;
// 示例2:源字符串长度等于指定长度
strncpy(dest, "HelloWorld", sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0'; // 手动添加终止空字符
cout << "Example 2: " << dest << endl;
// 示例3:源字符串长度大于指定长度
strncpy(dest, "Hello, World!", sizeof(dest));
dest[sizeof(dest) - 1] = '\0'; // 手动添加终止空字符
cout << "Example 3: " << dest << endl;
}
③memcpy:内存复制
0.作用
在内存中复制一块数据
1.函数原型
void* memcpy(void* dest, const void* src, size_t n);
2.参数:
①dest:目标内存区域的指针。
②src:源内存区域的指针。
③n:要复制的字节数。
3.返回值:
指向目标内存区域 dest 的指针
4.特点:
复制指定数量的字节,不关心数据内容。
可用于任何类型的数据,包括字符串、结构体、数组等。
目标和源内存区域不能重叠(重叠时应使用memmove)。
5.安全性:
strcpy可能导致缓冲区溢出(如果目标空间不够大),
memcpy需要确保目标区域有足够的空间,并且目标和源不重叠。
6.举例:
char dest[20];
char src[] = "Hello, World!";
memcpy(dest, src, strlen(src)+1); // +1 是为了复制终止空字符
④惯用法:复制字符串 (空字符也复制)
1.惯用法
while(*s1++ = *s2++)
;
2.完整版:
char* p = s1;
while(*s1++ = *s2++)
;
return p;
3.手动实现strcpy (重要)
char* mystrcpy(char* dest, const char* src){
char* p = dest; //保存指针的初始位置
while((*dest++ = *src++) != '\0')
;
return p;
}
4.完整版写法
char* Mystrcpy(char* dest, const char* src){
char* dest_copy = dest;
while(true){
*dest = *src;
if(*src == '\0') break;
dest++;
src++;
}
return dest_copy;
}
(3)strcmp
1.函数原型
int strcmp(const char *str1, const char *str2);
2.返回值
strcmp返回值是s1 - s2:
①若str1 < str2,返回值<0
②若str1 > str2,返回值>0
③若str1 == str2,返回值=0
3.比较规则
strcmp 函数通过逐个比较两个字符串的字符的 ASCII 值来确定谁大谁小。
①逐字符比较:从字符串的第一个字符开始,依次比较两个字符串的每一个字符。
②ASCII 值比较:比较字符的 ASCII 值,如果第一个不同的字符的 ASCII 值小于另一个字符串对应字符的 ASCII 值,则认为该字符串更小;反之,则认为该字符串更大。
③字符串长度:如果比较到一方字符串结束而另一方仍有剩余字符,则长度较短的字符串被认为更小。
(4)strcat
①strcat
1.函数原型
char* strcat(char *dest, const char *src);
举例:
strcat(s1, s2); //将s2拼接到s1中
①concatenate v.连接
②strcat可能数组越界,拼接后长度超出数组dest的长度
2.返回值
返回指向目标字符串dest的指针
②strncat
1.函数原型
char *strncat(char *dest, const char *src, size_t n);
举例:
strncat(s1, s2, count)
strcat(s1, "world\n"), MAXLINE - strlen(s1) -1); //1 for '\0'
s1[MAXLINE - 1] = '\0'; //记不住它到底会不会自动添加空字符,就一律手动添加一次
2.返回值
返回指向目标字符串dest的指针
(5)substr
1.函数原型
std::string substr (size_t pos = 0, size_t len = npos) const;
2.参数
①pos: 子串开始的位置 (默认为0)
②len: 要提取的子串的长度 (默认为 npos,表示直到字符串的末尾)
3.返回值
返回字串
4.举例
#include <string.h>
#include <iostream>
#include <string>
using std::cout;
using std::endl;
using std::string;
void test(){
string str1 = "Hello,World!";
string str2 = str1.substr(0,6); //从下标为0开始,提取长度为6
string str3 = str1.substr(6,6); //从下标为6开始,提取长度为6
cout << str2 << endl; //Hello,
cout << str3 << endl; //World!
}
int main()
{
test();
return 0;
}
5.字符串数组
字符串数组,即字符数组的数组,即二维字符数组
(1)二维字符数组

" "里的是一维字符数组的初始化式
弊端:
①空间浪费:字符串之间长度差异大,短的后面就要存很多’\0’,造成空间浪费
②不灵活:如交换字符串、字符串进行排序

(2)字符指针数组
字符指针数组,来存储字符串数组

" "里的是字符串字面值

缺点:多了一个存储字符数组
优点:
①节省了空间
②灵活
#include <stdio.h>
int main(void) {
//planets是数组,存储的元素是 char*, 字符指针 ,即指向字符的指针
char* planets[] = { "Mercury", "Venus", "Earth", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune" };
char* p = "Edward";
planets[2] = p;
int len = strlen(planets);
for (int i = 0; i < len; i++) {
printf("%s\n", planets[i]);
}
return 0;
}
6.命令行参数:argc、argv
1.命令行参数是什么?
操作系统调用可执行程序时,可以给它(main函数)传递的参数
①int argc:命令行参数的个数
②char* argv[]:命令行参数,字符串
2.要接收命令行参数,要修改main函数的参数:
int main(int argc, char* argv[]) {
//argc:argument count,命令行参数的个数
//argv:argument vector,命令行参数,字符串
//argv[0]是第一个命令行参数,一般为可执行程序的路径+程序名
printf("argc = %d\n", argc);
printf("argv[0] = %s\n\n", argv[0]);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
3.命令行参数的转换
sscanf是string scanf
int main(int argc, char* argv[]) {
int n;
float f;
sscanf(argv[1], "%d", &n);
sscanf(argv[2], "%f", &f);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}

4.如何在VS中设置命令行参数
项目[右键]→属性→调试→命令参数→[以空格间隔]→应用→确定
5.命令行参数和stdin的区别?
命令行参数在程序执行之前,stdin在程序执行中
6.命令行参数有什么作用?
①编写通用的程序 cp a.txt b.txt
②改变程序的行为 ls -l (传递不同的参数,程序展示不同的行为)
7.练习
(1)逆序输出字符串
CDay07第一题
(2)回文字符串
CDay07第二题
(十) 结构体、枚举
1.C语言最重要的三个组成部分:函数、指针、结构体
2.C语言中的聚合变量:
①数组 (同类的元素)
②结构体 (不同类的成员)
3.对象:
①属性:静态数据
②方法:行为
4.C语言结构体中只有属性,没有方法。
但是C可以通过指针,实现类似方法的功能

1.结构体 struct
(1)结构体变量的声明和初始化
1.定义结构体
结构体类型,是自定义类型
struct student{
int id;
char name[25];
int age;
char gender;
int chinese;
int math;
int english;
};
2.声明并初始化结构体变量:初始化式 { }
struct student s1 = {1, "xixi", 'F', 100}; //按位置赋值
struct student s2 = {2, "peanut", 'M'}; //未初始化的成员,默认为0值
(2)结构体的内存模型
(1)一片连续的内存空间
(2)按声明的顺序依次存放每一个成员
(3)成员之间 (在结构体变量的中间或后面),可能会进行填充,为了内存对齐

四个字节四个字节的传输时,如果不对齐,则int会被分割开,要读两次。
对齐的目的是为了减少读的次数,更快地访问数据
(3)结构体的操作:获取成员、赋值
(1)获取成员 .
(2)赋值 :结构体可以赋值(复制),数组不能赋值操作
s2 = s1;
赋值的本质 是 内存空间的复制。
在结构体很大时,复制结构体的开销很大。
传递参数和返回值时都会复制结构体,考虑只传递结构体的指针。

数组不支持赋值运算,而结构体支持赋值运算
(4)右箭头运算符-> 的由来
传递或返回一个结构体时,会导致结构体的复制。当结构体很大时,会增加很多开销。
所以C程序员往往会传递结构体的指针。
现在s是指针,*s才是结构体。则 s. 就要写成 (*s). ,先解引用 (结构体指针解引用为结构体),再获取成员
为了简便书写,将 (*s). 写作 s-> (语法糖)
格式: 指向结构体的指针 -> 结构体的成员
完整代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
struct student{
int id; //4
char name[25]; //25
char gender; //1
int chinese; //4
int math; //4
int english; //4
};
void print_stu_info(const struct student* s) {
/*printf("%d %s %c %d %d %d\n",(*s).id, (*s).name, (*s).gender,
(*s).chinese,(*s).math,(*s).english);*/
//语法糖: 用 s-> 代替 (*s).
printf("%d %s %c %d %d %d\n", s->id, s->name, s->gender,
s->chinese, s->math, s->english);
}
int main(void){
//声明并初始化变量
struct student s1 = { 1, "xixi", 'F', 100, 100, 100 };
struct student s2 = { 2,"peanut", 'M' };
print_stu_info(&s1); //s1是结构体, &s1是结构体的指针
print_stu_info(&s2);
return 0;
}
(5)给结构体起别名
typedef 类型 别名;
typedef struct student{
int id;
char name[25];
char gender;
int chinese;
int math;
int english;
} Student;
//请不要给指针类型起别名! 如下文的 *pStudent
typedef struct student{
int id;
char name[25];
char gender;
int chinese;
int math;
int english;
} Student, *pStudent;
(6)匿名结构体
匿名结构体:
没有标签,需要搭配 typedef 来形成新名字
typedef struct { //匿名结构体
int id;
char name[25];
char gender;
int chinese;
int math;
int english;
} Student;
(7)练习
结构体指针 Student* p 的作用:避免复制整个结构体
结构体指针数组 Student* pstudents[5] 的作用:通过交换指针数组中指针的顺序,堆学生按总成绩进行排序,避免了对结构体本身进行排序(移动),减少了开销

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
typedef struct student {
int number;
char name[25];
int chinese;
int math;
int english;
} Student;
void print_stu_info(const Student* p) {
printf("%d %s %d %d %d\n", p->number, p->name, p->chinese, p->math, p->english);
}
void highest(Student stu[], int n) {
int max_chinese = stu[0].chinese, max_math = stu[0].math, max_english = stu[0].english;
int index_chinese = 0, index_math = 0, index_english = 0;
for (int i = 1; i < 5; ++i) {
if (stu[i].chinese > max_chinese) {
max_chinese = stu[i].chinese;
index_chinese = i;
}
if (stu[i].math > max_math) {
max_math = stu[i].math;
index_math = i;
}
if (stu[i].english > max_english) {
max_english = stu[i].english;
index_english = i;
}
}
printf("语文最高分的同学的信息:");
print_stu_info(&stu[index_chinese]);
printf("数学最高分的同学的信息:");
print_stu_info(&stu[index_math]);
printf("英语最高分的同学的信息:");
print_stu_info(&stu[index_english]);
}
void average(Student stu[], int n) {
float sum_chinese = 0, sum_math = 0, sum_english = 0;
for (int i = 0; i < n; ++i) {
sum_chinese += stu[i].chinese;
sum_math += stu[i].math;
sum_english += stu[i].english;
}
printf("语文平均分:%.1f\n", sum_chinese / n);
printf("数学平均分:%.1f\n", sum_math / n);
printf("英语平均分:%.1f\n", sum_english / n);
}
int sum_score(const Student* p) {
return p->chinese + p->math + p->english;
}
void Bubble_des(Student* A[], int n) {
for (int i = 0; i < n - 1; ++i) {
for (int j = 0; j < n - 1 - i; ++j) {
if (sum_score(A[j]) < sum_score(A[j + 1])){ //对于降序,条件改为小于
Student* temp = A[j];
A[j] = A[j + 1];
A[j + 1] = temp;
}
}
}
}
int main(void) {
Student students[5]; //结构体数组,大小为5
for (int i = 0; i < 5; ++i) {
scanf("%d%s%d%d%d", &students[i].number, students[i].name,&students[i].chinese,
&students[i].math, &students[i].english);
}
printf("\n");
highest(students, 5);
printf("\n");
average(students, 5);
printf("\n");
//结构体 指针数组
Student* pstudents[5] = { students, students+1, students+2 ,students+3, students+4 };
Bubble_des(pstudents, 5);
for (int i = 0; i < 5; ++i) {
print_stu_info(pstudents[i]);
}
return 0;
}
2.枚举 enum
1.枚举类型,用来表示离散值,如 类型和状态。枚举类型是整数类型。
2.定义枚举类型
// 定义枚举类型
enum Suit{
// 罗列枚举值
DIAMONDS, //0 默认从0开始
HEARTS = 4, //4
SPADES = 10, //10
CLUBS //11 递增
};
3.给枚举类型起别名
typedef enum{
DIAMONDS,
HEARTS,
SPADES,
CLUBS
} Suit;


3723

被折叠的 条评论
为什么被折叠?



