C代码学习笔记(一)
1、头文件
#include <stdio.h> // 标准输入输出库(printf, scanf等)
#include <stdlib.h> // 标准库(malloc, free等)
#include <string.h> // 字符串处理库(strlen, strcpy等)
#include <math.h> // 数学函数
#include <time.h> // 时间函数
#include <ctype.h> // 字符处理
2、char*
1. 核心概念:char*
是什么?
char*
是一个指针,它指向一个或多个字符(char
)所在的内存地址。
你可以从两个主要角度来理解它:
指向单个字符的指针:
char c = 'A'; char* ptr = &c; // ptr 指向变量 c 的内存地址printf("%c\n", *ptr); // 输出: A (通过指针解引用获取它指向的值)
指向字符串的指针(更常见):
在 C 语言中,字符串是以空字符'\0'
结尾的字符数组。char*
常常被用来指向这样一个数组的起始地址。char* str = "Hello"; // str 指向字符串字面量 "Hello" 的第一个字符 'H' 的地址 printf("%s\n", str); // 输出: Hello
printf
的%s
格式说明符会从str
指向的地址开始,逐个打印字符,直到遇到'\0'
为止。
2. 类似语法和对比
理解 char*
的关键之一是把它和类似的语法结构进行对比。
a) char* str
vs char str[]
这是最容易混淆的一对。它们看起来很相似,但在内存分配和可修改性上有本质区别。
特性 | char* str = "Hello"; | char str[] = "Hello"; |
---|---|---|
内存位置 | str 是一个指针变量,存储在栈上。它指向的文字 "Hello" 存储在只读数据段(或常量区)。 | 整个数组 str 在栈上被分配内存,并初始化为 {'H','e','l','l','o','\0'} 。 |
可修改性 | 字符串内容不可修改(行为未定义,通常导致程序崩溃)。str[0] = 'h'; // ERROR! | 字符串内容可以修改。str[0] = 'h'; // OK! |
str 本身 | str 是一个变量,可以指向别的字符串。str = "World"; // OK! | str 是数组名,是一个常量指针,不能指向别处。str = "World"; // ERROR! |
sizeof | sizeof(str) 返回指针的大小(例如 4 或 8 字节)。 | sizeof(str) 返回整个数组的大小(6 字节,包含 \0 )。 |
简单比喻:
char* str
:像一张写着朋友家地址的纸条。你可以擦掉纸条上的地址,换成另一个朋友的地址(str = "World"
)。但你不能根据这个地址去修改朋友家的房子(str[0] = 'h'
)。char str[]
:像你自己家的一块地皮和建好的房子。你不能把整块地皮搬到别处去(str = "World"
),但你可以随意装修自己家的房子(str[0] = 'h'
)。
b) const char*
由于用 char*
指向字符串字面量有修改的风险(这是危险的),最佳实践是总是使用 const char*
。
const char* str = "Hello";
// str[0] = 'h'; // 编译器会直接报错,避免了运行时崩溃
这明确告诉编译器和你自己:str
指向的数据是常量,不可修改。
3. 常见用法示例
a) 遍历字符串
#include <stdio.h>int main() {const char* s = "Hello";// 方法 1: 数组下标法for (int i = 0; s[i] != '\0'; i++) {printf("%c ", s[i]);}printf("\n");// 方法 2: 指针算术法 (更C风格)const char* p = s; // p 指向字符串开头while (*p != '\0') { // 只要 p 指向的字符不是结束符printf("%c ", *p);p++; // 让 p 指向下一个字符的地址}return 0;
}
b) 作为函数参数
函数需要处理字符串时,通常传入 const char*
(如果不想修改字符串)或 char*
(如果需要修改字符串)。
// 计算字符串长度
int my_strlen(const char* str) {int len = 0;while (*str != '\0') {len++;str++; // 指针移动到下一个字符}return len;
}// 修改字符串内容
void to_uppercase(char* str) {while (*str != '\0') {if (*str >= 'a' && *str <= 'z') {*str = *str - ('a' - 'A'); // 转换为大写}str++;}
}int main() {const char* s1 = "hello";printf("Length: %d\n", my_strlen(s1)); // 输出 5char s2[] = "hello"; // 必须是数组才能修改to_uppercase(s2);printf("%s\n", s2); // 输出 "HELLO"return 0;
}
4. 动态内存分配
char*
也常用于动态分配内存来创建字符串,这在需要可变长度字符串时非常有用。
#include <stdlib.h>
#include <string.h>
#include <stdio.h>int main() {// 分配足够的内存来存储 "Hello" (5字符 + 1空字符 = 6字节)char* str = (char*)malloc(6 * sizeof(char));// 检查分配是否成功if (str == NULL) {printf("Memory allocation failed!\n");return 1;}// 复制字符串到新分配的内存中strcpy(str, "Hello");printf("%s\n", str); // 输出: Hello// 可以安全地修改,因为它在堆上str[0] = 'h';printf("%s\n", str); // 输出: hello// 非常重要:使用完后释放内存free(str);str = NULL; // 避免野指针return 0;
}
总结
概念 | 描述 | 关键点 |
---|---|---|
char* | 指向字符或字符串的指针。 | 理解指针本身和它指向的数据。 |
char* str | 指向(通常是只读的)字符串字面量。 | 字符串不可修改,指针可指向别处。 |
char str[] | 一个字符数组。 | 字符串可修改,数组名不可指向别处。 |
const char* | 指向常量字符串的指针。 | 安全,明确意图,推荐使用。 |
动态分配 | 使用 malloc 为 char* 在堆上分配内存。 | 字符串可修改,必须手动 free 。 |
3、字符串长度获取
strlen()
:获取字符串长度(不包括结尾的'\0'),返回字符串中字符的个数
示例:
char text[] = "Hello";
int len = strlen(text); // len = 5// sizeof 与 strlen 的区别
char arr[10] = "Hi";
int size1 = sizeof(arr); // 10 (数组总大小)
int size2 = strlen(arr); // 2 (实际字符串长度)
4、malloc
malloc
(Memory ALLOCation)是 C 标准库 <stdlib.h>
中的一个函数,用于在堆(Heap) 上动态地分配指定大小的内存块。
成功时返回指向分配内存起始地址的 void*
指针,失败时返回 NULL
// 分配整型数组
int* numbers = malloc(10 * sizeof(int));// 分配结构体
typedef struct {int id;char name[20];
} Person;
Person* person = malloc(sizeof(Person));// 记得检查分配是否成功
int* ptr = malloc(100 * sizeof(int));
if (ptr == NULL) {printf("内存分配失败!\n");exit(1);
}
为字符串分配内存
#include <stdlib.h>
#include <stdio.h>int main() {// 分配存储 "Hello" 的内存:5个字符 + 1个空字符 '\0'char* str = (char*)malloc(6 * sizeof(char));if (str == NULL) {printf("内存分配失败!\n");return 1;}// 使用内存...free(str);str = NULL;return 0;
}
重要注意事项
必须检查返回值:
malloc
可能失败(内存不足时)必须释放内存:用
free()
释放,否则内存泄漏类型转换:
malloc
返回void*
,需要转换为具体类型计算大小:使用
sizeof
确保跨平台兼容性
内存操作函数
a) memcpy
- 内存复制
void* memcpy(void* dest, const void* src, size_t num);
// 按字节复制,不关心 '\0'
b) memmove
- 安全内存移动
void* memmove(void* dest, const void* src, size_t num);
// 处理内存重叠的情况
c) memset
- 内存设置
void* memset(void* ptr, int value, size_t num);
// 将内存块设置为特定值
示例
int cnt[26]; // 声明一个包含26个整数的数组
memset(cnt, 0, sizeof(cnt)); // 将数组的所有元素设置为0
int cnt[26];
声明一个名为
cnt
的数组数组包含26个
int
类型的元素通常用于存储26个英文字母的计数(a-z)
memset(cnt, 0, sizeof(cnt));
memset()
:内存设置函数,用于将一块内存区域填充为指定的值第一个参数
cnt
:要设置的内存起始地址(这里就是数组首地址)第二个参数
0
:要填充的值(这里用0填充)第三个参数
sizeof(cnt)
:要设置的字节数sizeof(cnt)
返回整个数组占用的字节数如果
int
是4字节,那么sizeof(cnt) = 26 × 4 = 104
字节
5、strcpy
strcpy
(STRING COPY)是 C 标准库 <string.h>
中的函数,用于将一个字符串(包括终止的空字符 '\0'
)复制到另一个字符串。
函数原型
char* strcpy(char* destination, const char* source);
参数1:
destination
- 目标字符串的指针(必须有足够空间)参数2:
source
- 源字符串的指针(不会被修改)返回值:返回目标字符串的指针
#include <stdio.h>
#include <string.h>int main() {char source[] = "Hello World";char destination[20]; // 确保目标有足够空间strcpy(destination, source);printf("源字符串: %s\n", source); // Hello Worldprintf("目标字符串: %s\n", destination); // Hello Worldreturn 0;
}
strcpy
最大的问题是不检查目标缓冲区的大小,如果源字符串比目标缓冲区长,会导致缓冲区溢出,这是严重的安全漏洞。
安全替代方案:strncpy
char* strncpy(char* destination, const char* source, size_t num);
参数1:
destination
- 目标缓冲区参数2:
source
- 源字符串参数3:
num
- 最多复制的字符数返回值:目标字符串的指针
特点和行为
复制最多
num
个字符从源到目标如果源字符串长度小于
num
:用'\0'
填充剩余空间如果源字符串长度大于等于
num
:不会自动添加'\0'
不会检查目标缓冲区大小(但比
strcpy
安全)
// 安全使用 strncpy 的模板
char destination[SIZE];
strncpy(destination, source, sizeof(destination) - 1);
destination[sizeof(destination) - 1] = '\0'; // 确保终止
其他重要字符串函数
a) strcpy
- 基本字符串复制
char* strcpy(char* dest, const char* src);
// 危险:不检查缓冲区大小
b) strcat
- 字符串拼接
char* strcat(char* dest, const char* src);
// 将 src 追加到 dest 末尾
c) strncat
- 安全字符串拼接
char* strncat(char* dest, const char* src, size_t num);
示例:
char dest[20] = "Hello";
strncat(dest, " World!", sizeof(dest) - strlen(dest) - 1);
// 自动添加 '\0',比 strncpy 更友好
d) strcmp
- 字符串比较
int strcmp(const char* str1, const char* str2);
// 返回 0:相等, >0:str1>str2, <0:str1<str2
e) strncmp
- 安全字符串比较
int strncmp(const char* str1, const char* str2, size_t num);
// 比较前 num 个字符
f) strlen
- 字符串长度
size_t strlen(const char* str);
// 返回字符串长度(不包括 '\0')
g) strchr
- 查找字符
char* strchr(const char* str, int character);
// 返回字符第一次出现的位置,找不到返回 NULL
h) strstr
- 查找子串
char* strstr(const char* haystack, const char* needle);
// 返回子串第一次出现的位置
#include <stdio.h>
#include <string.h>int main() {char haystack[] = "hello world";char needle[] = "world";char* result = strstr(haystack, needle);printf("haystack地址: %p\n", haystack);printf("result地址: %p\n", result);printf("偏移量: %ld\n", result - haystack);printf("下标: %d\n", (int)(result - haystack));printf("找到的字符串: %s\n", result);return 0;
}haystack地址: 0x7ffd5a3b2a10
result地址: 0x7ffd5a3b2a16
偏移量: 6
下标: 6
找到的字符串: world
i) strtok
- 字符串分割
char* strtok(char* str, const char* delimiters);
// 用于分割字符串
#include<stdio.h>
#include<string.h>
#include<stdlib.h>char* findTheDifference(char* s, char* t) {int n = strlen(s), m = strlen(t);char* str = malloc(n + m + 1);strcpy(str,s);strcat(str,t);return str;}int main () {char* result = findTheDifference("Hello", "World");printf("结果:%s\n", result);free(result);return 0;
}
7、位运算符
假设如果 A = 60,且 B = 13,现在以二进制格式表示,它们如下所示:
A = 0011 1100
B = 0000 1101
-----------------
A&B = 0000 1100
A|B = 0011 1101
A^B = 0011 0001
~A = 1100 0011
下表显示了 C 语言支持的位运算符。假设变量 A 的值为 60,变量 B 的值为 13,则:
运算符 | 描述 | 实例 |
---|---|---|
& | 对两个操作数的每一位执行逻辑与操作,如果两个相应的位都为 1,则结果为 1,否则为 0。 按位与操作,按二进制位进行"与"运算。运算规则: 0&0=0; 0&1=0; 1&0=0; 1&1=1; | (A & B) 将得到 12,即为 0000 1100 |
| | 对两个操作数的每一位执行逻辑或操作,如果两个相应的位都为 0,则结果为 0,否则为 1。 按位或运算符,按二进制位进行"或"运算。运算规则: 0|0=0; 0|1=1; 1|0=1; 1|1=1; | (A | B) 将得到 61,即为 0011 1101 |
^ | 对两个操作数的每一位执行逻辑异或操作,如果两个相应的位值相同,则结果为 0,否则为 1。 异或运算符,按二进制位进行"异或"运算。运算规则: 0^0=0; 0^1=1; 1^0=1; 1^1=0; | (A ^ B) 将得到 49,即为 0011 0001 |
~ | 对操作数的每一位执行逻辑取反操作,即将每一位的 0 变为 1,1 变为 0。 取反运算符,按二进制位进行"取反"运算。运算规则: ~1=-2; ~0=-1; | (~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。 |
<< | 将操作数的所有位向左移动指定的位数。左移 n 位相当于乘以 2 的 n 次方。 二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。 | A << 2 将得到 240,即为 1111 0000 |
>> | 将操作数的所有位向右移动指定的位数。右移n位相当于除以 2 的 n 次方。 二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补 0,负数左补 1,右边丢弃。 | A >> 2 将得到 15,即为 0000 1111 |
A ^= B;
// 等价于
A = A ^ B;