- 以下 a 输出的值是多少
int a = 0x11223344;
char *pc = (char *)&a;
*pc = 0;
printf("%x\n", a); //0x 11223300
- 最大对齐数为 8 字节,计算以下结构体大小: 12
- atoi: 将字符串转换为 int
atoi 函数会从字符串的开头开始扫描,跳过任何空白字符,直到遇到第一个非空白字符。然后,它会解析可选的正负号,接着解析连续的数字字符,直到遇到非数字字符为止。最后,将解析到的数字字符转换为整数并返回。
- 小端字节序:把数据的高字节放到高地址,低字节放到地地址; 大端字节序相反; 以 0x11223344 为例, 高位为11,放到高地址; 低位为 44,放到低地址
- 数据在内存中的存储
unsigned int a = 200;
unsigned int b = 100;
unsigned char c = a + b;
//输出 300,44
printf("%d,%d", a +b, c);
-
strlen 取的是 \0 之前字符的个数,如果没有 \0, 取得的值是随机值
-
分析以下代码输出结果
void pointer_test() {
int aa[2][5] = {10,9,8,7,6,5,4,3,2,1};
//&aa 是二维数组的地址, +1 跳过整个二维数组
int *ptr1 =(int *)(&aa + 1);
//aa 是首元素地址,也就是第一行一维数组的地址, +1 跳过第一行的地址,为第二行首元素的地址
int *ptr2 =(int *)(*(aa + 1));
//随机值 5 1 6
printf("%d %d %d %d\n",*ptr1,*ptr2,*(ptr1-1),*(ptr2-1));
}
- p 中存放的是 hello 的第一个字符 'h' 的 "地址", *p 取出的是 h 这个字符
char *p = "hello";
printf("*p:%c\n",*p); //输出 h
printf("p:%p\n", p); //输出 h的地址
printf("p:%s\n",p); //输出 hello
- 猜凶手
题目名称:
猜凶手
题目内容:
日本某地发生了一件课荣案,警察通过排查确定杀人凶手必为4个嫌疑犯的一个以下为4个嫌疑犯的供词:
A说:不是我,
B说:是C
C说:是D.
D说:C在胡说
已知3个人说了真话,1个人说的是假话,
现在请根据这些信息,写一个程序来确定到底谁是凶手,
char findKiller() {
char killer = 'a';
for (; killer <= 'd'; killer++) {
if ((killer != 'a') + (killer == 'c') + (killer == 'd') + (killer != 'd') == 3) {
// printf("凶手是%d",killer);
return killer;
}
}
return '\0';
}
char killer = findKiller();
if (killer != '\0') {
printf("凶手是:%c\n",killer);
}else {
printf("未找到凶手\n");
}
- 从排序好的二维数组中查找元素
/**
* 在二维数组中查找指定元素
* @param arr 二维数组
* @param rows 数组的行数
* @param cols 数组的列数
* @param key 要查找的元素
* @return 1 找到元素,0 未找到元素
*/
int findNum(int arr[][3], int rows, int cols, int key) {
int i = 0; // 从第一行开始
int j = cols - 1; // 从最后一列开始
// 从右上角开始查找
while (i < rows && j >= 0) {
if (arr[i][j] < key) {
// 当前元素小于 key,向下移动一行
i++;
} else if (arr[i][j] > key) {
// 当前元素大于 key,向左移动一列
j--;
} else {
// 找到目标元素
return 1;
}
}
// 未找到目标元素
return 0;
}
/**
* 测试 findNum 函数
*/
void findNum_test() {
int arr[][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int rows = sizeof(arr) / sizeof(arr[0]); // 计算行数
int cols = sizeof(arr[0]) / sizeof(arr[0][0]);
// 测试用例 1:查找存在的元素
int ret1 = findNum(arr, rows, cols, 5);
printf("Find 5: %d\n", ret1); // 预期输出: 1
// 测试用例 2:查找不存在的元素
int ret2 = findNum(arr, rows, cols, 10);
printf("Find 10: %d\n", ret2); // 预期输出: 0
// 测试用例 3:查找边界元素
int ret3 = findNum(arr, rows, cols, 1);
printf("Find 1: %d\n", ret3); // 预期输出: 1
int ret4 = findNum(arr, rows, cols, 9);
printf("Find 9: %d\n", ret4); // 预期输出: 1
}
- 调整奇数偶数顺序
/**
* 调整奇数偶数顺序,是所有奇数位于偶数前面
* 数组中元素为 1,2,3,4,5,6 变成 1,3,5,2,4,6
*/
void adjust_odd_even_num(int *ptr, int len) {
int left = 0;
int right = len - 1;
while (left < right) {
//从前往后遇到偶数了,要换位置
while (left < right && ptr[left] % 2 != 0) {
left++;
}
//从后往前遇到奇数了,要换位置
while (left < right && ptr[right] % 2 == 0) {
right--;
}
int tmp = ptr[left];
ptr[left] = ptr[right];
ptr[right] = tmp;
}
}
void adjust_odd_even_num_test() {
int arr[] = {1,2,3,4,5,6};
size_t len = sizeof(arr) / sizeof(arr[0]);
adjust_odd_even_num(arr,len);
for (int i = 0; i< len;i++) {
printf("%d ",arr[i]);
}
printf("\n");
}
- 左旋算法
/**
* 方式一:
* @param str
* @param k 旋转 k 个字符
*/
void left_round(char *str, int k) {
size_t len = strlen(str);
size_t time = k % len;
printf("len:%zu,k:%d,time:%zu\n",len,k,time);
int i = 0;
int j = 0;
for (i = 0; i < time;i++) {
//先把第一个元素存起来
char temp = str[0];
for (j = 0 ; j < len -1;j++) {
str[j] = str[j+1];
}
//ba第一个元素 temp 的值放到最后一个位置
str[j] = temp;
}
}
/**
*方式二:
* str: ABCDEF ,k:3 变成 DEFABC
* 步骤1: 先取出 DEF 放到临时数组中 tmp 中
* 步骤2: 取出 ABC 拼接到 tmp 后面
* 步骤3: 把 tmp 的值赋值给 str
* @param str
* @param k
*/
void left_round2(char *str, int k) {
char tmp[256] = {0};
size_t len = strlen(str);
size_t time = k % len;
// printf("len:%zu,k:%d,time:%zu\n",len,k,time);
//步骤1: 从第 time 个元素开始拷贝到 tmp 中
strcpy(tmp,str + time);
//步骤2:把从 0开始的time 个字符拼接到 tmp 的后面
strncat(tmp,str,time);
//步骤3: 把 tmp 的值赋值给 str
strcpy(str,tmp);
}
方式3:
void reverseStr(char *str, int left, int right) {
while (left < right) {
char tmp = str[left];
str[left] = str[right];
str[right] = tmp;
left++;
right--;
}
}
int len = strlen(str);
int time = k % len;
reverseStr(str,0,time -1);
reverseStr(str,time,len -1);
reverseStr(str,0,len-1);
-
NDEBUG必须在#include <assert.h>之前定义,才能禁用assert。
#define NDEBUG
#include <assert.h>
void reverse() {
char *str = NULL;
assert(str != NULL);
printf("reverse");
}
- 字符串逆序
void reverse(char *str) {
assert(str);
size_t len = strlen(str);
char *left = str;
char *right = str + len - 1;
while (left < right) {
char temp = *left;
*left = *right;
*right = temp;
left++;
right--;
}
}
// char *str = "hello world"; //非法: 字符串字面量(char* str = "hello world")是只读的,不能修改其内容
char str[] = "hello world"; //合法:字字符数组,存储在可写内存区域(通常是栈或堆)。可以安全地修改其内容。
reverse(str);
char str[10000] = {0};
//gets 可以读取包含空格的字符, scanf 是遇到空格就结束
while (fgets(str,sizeof(str),stdin)) {
reverse(str);
printf("%s\n", str);
printf("请输入要翻转的字符串:");
}
- int * const p 和 int const *p 的区别
int * const p: 指针 `p` 是常量,不能改变其指向的地址,但可以修改所指向的对象的值。
int const * p: 指针 `p` 不是常量,可以改变其指向的地址,但不能修改所指向的对象的值。
int a = 10;
int b = 20;
int * const p = &a;
*p = 30; // 合法,修改 a 的值为 30
p = &b; // 非法,p 是常量指针,不能改变其指向的地址
printf("a:%d\n",a); //输出 30
int const *p2 = &a;
p2 = &b; //合法,p 可以指向不同的地址,此时 *p2 解引用之后得到的值是b,也就是 20
*p2 = 30; //非法,p 指向的对象是常量,不能修改其值
关键词总结
- **`int * const p`**:
- 关键词:**指针是常量**。
- 特点:指针不能改,对象可以改。
- **`int const * p`**:
- 关键词:**指向是常量**。
- 特点:指针可以改,对象不能改。
-
局部变量不初始化就是野指针, 在 32 位机器上,int 类型的变量占用 4个字节,指针占用4个字节,可使用的最大内存空间是 2 的 32 次方;64位机器上,int 类型的变量占用 4个字节,指针占用8个字节,可使用的最大内存空间是 2 的 64 次方;
-
分析以下代码的输出值
int arr[] = {1,2,3,4,5};
short *p = (short *) arr;
for (int i = 0 ; i < 4;i++) {
//p+i 每次访问的是 2 个字节r
*(p+i) = 0;
}
for (int i = 0 ; i < 5;i++) {
printf("%d ",arr[i]); //输出 0 0 3 4 5
}
- 测试一下 arr 中的元素
int arr4[] = {1,2,(3,4),5}; //实际存的是 1,2,4,5
int size = sizeof(arr4) / sizeof(arr4[0]); 、、4
printf("arr4:%zu,size:%d\n",sizeof arr4,size); //sizeof arr4 为 16
for (int i = 0; i<size;i++) {
//输出 1,2,4,5
printf("%d ",arr4[i]);
}
- 以下字符数组区别
char acx[] = "abcdefg"; //存的是 a b c d e f g \0, 共 8 个字节
char acy[] = {'a','b','c','d','e','f','g'}; //存的是 a b c d e f g 没有 \0,共 7 个字节
printf("sizeof acx:%zu\n",sizeof acx); //8
printf("sizeof acy:%zu\n",sizeof acy); //7
-
随着数组下标的增大,地址由低到高
-
一维数组初始化
int arr[10] = {1,2,3,4,5,6};
int arr[] = {1,2,3,4,5,6};
int arr[10]={0};
-
scanf 的占位符 %s 表示读取一个字符串,遇到空格就结束了
-
库文件包含
查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误。
这样是不是可以说,对于库⽂件也可以使⽤ “” 的形式包含?
答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件
了。
- 条件编译
/**
* 条件编译
*/
void conditional_compile() {
//条件编译
#if MAX > 10
printf("hello\n");
#endif
//多个分支的条件编译
#if M==0
printf("000\n");
#elif M==1
printf("1111\n");
#elif M==2
printf("2222\n");
#else
printf("33333\n")
#endif
//检查符号是否被定义,定义了就参考编译,否则不参与编译
#if defined N
printf("定义了M");
#endif
#if !defined N
printf("未定义M");
#endif
//检查符号是否未被定义过
#ifndef N
printf("未定义M");
#endif
}
- 命令行定义
许多C 的编译器提供了⼀种能⼒,允许在命令⾏中定义符号。⽤于启动编译过程。
例如:当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处。(假定某
个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个
机器内存⼤些,我们需要⼀个数组能够⼤些。)
int arr[SZ]; // SZ 是未定义
int i =0;
for (i = 0; i< SZ;i++) {
arr[i] = i +1;
}
for (i = 0; i< SZ;i++) {
printf("%d ",arr[i]);
}
编译指令:
gcc test.c -D SZ=10 -o test
其中 -D 用于定义代码中未定义的符号
`-D` 是 GCC 的一个选项,用于在编译时定义一个宏。
`SZ=10` 表示定义一个名为 `SZ` 的宏,并将其值设置为 `10`。
- undef 移除一个宏
- 运算符 ##
## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称
为记号粘合
这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。
#define GENERIC_MAX(type) \
type type##_max(type x,type y) \
{ \
return x>y?x:y;\
}
//定义函数
GENERIC_MAX(int); //生成 int_max
GENERIC_MAX(float); //生成 float_max
int r1 = int_max(1,2);
printf("%d\n",r1);
float r2 = float_max(3.1f,4.5f);
printf("%f\n",r2);
- 运算符 #
运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。
#运算符所执⾏的操作可以理解为”字符串化“。
当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 .
就可以写:
#define PRINT(n) printf("the value of "#n " is %d", n);
#define Print(n,format) printf("the value of" #n" is " format "\n",n)
int a = 1;
// printf("the value of a is %d\n",a);
Print(a,"%d");
int b = 2;
// printf("the value of b is %d\n",b);
Print(b,"%d");
float f = 5.6f;
// printf("the value of f is %f\n",f);
Print(f,"%f");
- 宏和函数对比
#define Malloc(n,type) (type*)malloc(n*sizeof(type))
int *p2 = Malloc(10,int);
//等价于
int *p = (int *)malloc(10 *sizeof(int));
- 观察编译链接流程
gcc test.c -E -o test.i //预编译、预处理流程, 预处理命令的处理,注释删除,行号,文件标识符
gcc test.i -S -o test.s //编译, 词法分析,语法分析, 将 C 语言代码转换成汇编代码
gcc test.s -S -o test.o //汇编,将汇编代码翻译成二进制指令(机器指令)
- 预处理符号
void preprocessor_symbol() {
printf("file:%s\n",__FILE__);
printf("line:%d\n",__LINE__);
printf("date:%s\n",__DATE__);
printf("time:%s\n",__TIME__);
}
- 编译和链接
- 总结C/C++中程序内存区域划分
C/C++程序内存分配的几个区域:
1、栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。《函数栈帧的创建和销毁》
2、堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。
3、数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。3.
4、代码段:存放函数体(类成员函数和全局函数)的二进制代码。
- 柔性数组
struct st_type {
int i;
int arr[0]; //未知大小的数组, arr 是柔性数组的成员
};
void test() {
//结构体大小不包括柔性数组成员大小
printf("%zu\n",sizeof(struct st_type));
st_type_t *ps = (struct st_type *) malloc(sizeof(struct st_type) + 20 * sizeof(int));
if (ps == NULL) {
perror("malloc failed");
return;
}
ps->n = 100;
int i = 0;
for (i = 0; i < 20;i++) {
ps->arr[i] = i +1;
}
//调整 ps 指向空间的大小
st_type_t * ptr = realloc(ps,sizeof(struct st_type) + 40 * sizeof(int));
if (ptr == NULL) {
perror("realloc failed");
return;
}
ps = ptr;
ptr = NULL;
for (i = 0; i < 40;i++) {
//只有前 20 个元素有值,后面的都是随机值
printf("%d ",ps->arr[i]);
}
free(ps);
ps = NULL;
}
在“结构体中”,最后一个元素是 未知大小的 数组,该元素称为柔性数组的成员.
柔性数组的特点:
结构中的柔性数组成员前面必须至少一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
包含柔性数组成员的结构用malloc(函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
-
free 之后要记得把指针置为 NULL
-
常见的动态内存错误
对NULL指针的解引用操作
对动态开辟空间的越界访问
对非动态开辟内存使用free释放
使用free释放一块动态开辟内存的一部分
对同一块动态内存多次释放
动态开辟内存忘记释放(内存泄漏)
- 检查以下代码是否有错误
void GetMmeory(char *p) {
p = (char *)malloc(100);
}
void test() {
printf("test start\n");
char *str = NULL;
//这里其实是传的 str 的拷贝
GetMmeory(str);
printf("str 111:%s\n",str);
strcpy(str, "hello world");
printf("str 222:%s\n",str);
以上代码运行会报错,原意是通过 GetMmeory 改变 str 的值,但是由于传的不是地址,而是值的拷贝,所以实际在执行完 GetMmeory 时, str 的值还是为 NULL
修改后的代码如下:
方式 一:
void GetMmeory2(char **p) {
*p = (char *)malloc(100);
}
void test2() {
printf("test start\n");
char *str = NULL;
GetMmeory2(&str);
printf("str 111:%s\n",str);
strcpy(str, "hello world");
printf("str 222:%s\n",str);
free(str);
str = NULL;
}
方式 二:
char *getMemory3() {
return (char *)malloc(100);
}
printf("test start\n");
char *str = NULL;
//这里其实是传的 str 的拷贝
str = getMemory3();
strcpy(str, "hello world");
printf("str:%s\n",str);
free(str);
str = NULL;
- free
free函数⽤来释放动态开辟的内存,非动态开辟的内存不能使用 free 释放。 如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的。 如果参数 ptr 是NULL指针,则函数什么事都不做。 malloc 和 free 成对使用 free的参数必须是内存空间的起始地址
- malloc
int *p = (int *) malloc(INTMAX_MAX); //模拟分配内存失败
if (p != NULL) {
printf("内存分配成功,起始地址:%p\n",p);
}else {
perror("内存分配失败");
return ;
}
- 联合体大小计算
联合的⼤⼩⾄少是最⼤成员的⼤⼩。
当最⼤成员⼤⼩不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍
-
位段的内存分配 位段的成员可以是 intsigned int 或者是 char 等类型unsigned int2、位段的空间上是按照需要以4个字节(int )或者1个字节(char )的方式来开辟的。位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
-
结构体实现位段 位段的声明和结构体类似, 但有 2 个不同: 1、位段的成员必须是 int ,unsigned int ,signed int、char,在 c99 中位段的声明也可以是其他类型 2、位段的成员后面有一个冒号和数字
struct A {
int _a: 2; //2 个字节
int _b: 5; //5 个字节
int _c: 10;//10 个字节
int _d: 30;//30 个字节
};
//输出占用 8 个字节
printf("%zu\n", sizeof(struct A));
- 结构体传参
推荐传递结构体地址,而不是结构体本身, 原因是:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下 降, 如果不想要修改结构体的值,可以在参数上添加 const 关键字
- 修改结构体对齐数
//vs上默认的对齐数是 8,这里改为 1
#pragma pack(1)
struct S5 {
char c1; //1 1 1 分别代表 自身大小、默认对消、对齐数
int n; //占用4个字节,对齐数 为 1,从 1 的位置开始存放 4个字节
char c2; //占用1个字节,对齐数 为 1,从 5 的位置开始存放 1个字节,
// 由于结构体的大小为最大对齐数(这里最大对齐数为1)的整数倍,值为 6
};
//恢复对齐数
#pragma pack()
- 如何优化结构体的内存
让空间小的成员尽量放在一起
- 结构体的内存对齐规则
1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。
- VS 中默认的值为 8
- Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的
整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构
体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
- 为什么要做内存对齐 用空间换取时间
- 理解以下代码
int a[4] = {1, 2, 3, 4};
int *ptr1 = (int *) (&a + 1);
printf("%p,%x",&ptr1[-1], ptr1[-1]);
int *ptr2 = (int *) ((int) a + 1);
printf("%x", *ptr2);
-
signed char 的范围为 -128 ~127, unsigned char 的范围为 0 ~255,
-
有符号数据类型,用 %d, 无符号数据类型,用 u%打印
-
以下分别用 %u 和 %d 输出 a 的值是多少
//100000000 00000000 00000000 10000000 - -128 原码
//111111111 11111111 11111111 01111111 - -128 反码
//111111111 11111111 11111111 10000000 - -128 补码
char a = -128;
//%u 打印无符号的整数,%u的角度,他认为内存中存储的是无符号的整数
//由于 a 是 char 智能放 8 个 bit,因此 a 补码的值是: 10000000
//%u 是有符号整数,因此高位补 1,变成 111111111 11111111 11111111 10000000
//另外由于 %u 是无符号数,因此补码、反码和源码都相同,所以 a 的原码也是 111111111 11111111 11111111 10000000,这个值用
//10 进制表示为 4294967168
printf("%u\n",a); //输出 4294967168
//如果是 %d打印,则是 -128, 将 111111111 11111111 11111111 10000000 转换成 原码
//为 100000000 00000000 00000000 10000000 ,值为 -128
printf("%d\n",a); //输出 -128
- 以下 a,b,c 的值是多少
关键知识点: 有符号数,高位补 1, 无符号数,高位补 0
//char 是有符号还是无符号,取决于编译器,大部分编译器都是有符号的 signed
//char a 相当于 signed char a
//10000000 00000000 00000000 00000001 //-1的原码
//11111111 11111111 11111111 11111110 //-1的反码
//11111111 11111111 11111111 11111111 //-1的补码
//char a 只占用一个字节 8 位,取得是末尾的 11111111
//%d 打印有符号有符号整型, a 是char 有符号类型,做整型提升,因此高位全补 1 , 变成 11111111 11111111 11111111 11111111
//打印 a 时打印的是原码:10000000 00000000 00000000 00000001,因此是 -1
char a = -1;
signed char b = -1;
//-1 的补码: 11111111 11111111 11111111 11111111,char a 只占用一个字节 8 位,取得是末尾的 11111111
//c 是无符号,做整型提升时,高位全部 0, 变成00000000 00000000 00000000 11111111, 因此补码值是正数的 255
//正数的补码、反码、原码都相同,因此 %d 输出 c 的值是 255
unsigned char c = -1;
//a:-1, b:-1, c:255
printf("a:%d,b:%d,c:%d\n",a,b,c);
- 判断大小端
int check_style() {
//a 为 1,,16 进制表示为: 0x 00 00 00 01, 00为高地址, 01为低地址。
//如果是大端存储(高位存低地址,低位存高地址),存储的是 00 00 00 01,
//如果是小端存储(高位存高地址,低位存低地址),存储的是 01 00 00 00
//因此可以判断第一个字节是否为 1 来判断是大端还是小端存储
int a = 1;
if (*(char *)&a == 1) {
printf("小端");
}else {
printf("大端");
}
return 0;
}
int check_style2() {
//a 为 1,,16 进制表示为: 0x 00 00 00 01, 00为高地址, 01为低地址。
//如果是大端存储(高位存低地址,低位存高地址),存储的是 00 00 00 01,
//如果是小端存储(高位存高地址,低位存低地址),存储的是 01 00 00 00
//因此可以判断第一个字节是否为 1 来判断是大端还是小端存储
int a = 1;
//返回 1 是小端,返回 0 是大端
return *(char *) &a;
}
- 整型在内存中的存储 整数有原码、反码、补码,计算机中存储和计算都是用的补码,原因是 使用补码可以将 符号位和数据域统一处理
大端字节序存储:将一个数据的低位字节内容存放在内存的高地址处,高位字节的内容存放在低地址处 小端字节序存储:将一个数据的低位字节内容存放在内存的低地址处,高位字节的内容存放在高地址处
例如 0x11223344, 如果是按照 11223344 存储,说明是大端存储, 如果是按照 44332211 存储,说明是小端存储
我们常用的 x86 的通常是小端存储,而 Keil C51 则为大端模式,很多 ARM ,DSP 的为小端模式
- memcmp 比较指针的前 num 个字符的 ASCII 码值的大小
char str1[] = "abcdefg";
char str2[] = "abcdefh";
size_t size = 0;
if (strlen(str1) > strlen(str2)) {
size = strlen(str1);
}else {
size = strlen(str2);
}
int ret = memcmp(str1,str2,size -1);
if (ret > 0) {
printf("%s 比 %s 大",str1,str2);
}else if (ret < 0) {
printf("%s 比 %s 小",str1,str2);
}else {
printf("%s 和 %s 相等",str1,str2);
}
- memset memset 是用来设置内存的,以字节为单位设置成想要的内容
char str[] = "hello world";
memset(str + 6,'x',5);
//输出 hello xxxxx
printf("str:%s\n",str);
int arr[] = {1,2,3,4,5};
//原意是想把 arr 中前 4个元素的值改为 1, 但实际只会改变第一个元素的值,且是把第一个元素的 4个字节全部改为 1,变成
//0x01010101, 也就是 16843009
memset(arr,1,4);
//结果输出 16843009 2 3 4 5
print_array(arr,sizeof(arr) / sizeof(arr[0]));
-
C 语言规定,memcpy 只要能实现不重叠的拷贝就行,重叠的拷贝交给 memmove
-
memcpy 使用和模拟实现 从 src 的位置向后拷贝 n 个字节的数据到 dest 指向的内存位置
void *my_memcpy(void *dest, const void *src, size_t num) {
assert(dest != NULL);
assert(src != NULL);
void *ret = dest;
while (num--) {
*(char*)dest = *(char*)src;
dest = (char *)dest + 1;
src = (char *)src + 1;
}
return ret;
}
int src[] = {1, 2, 3, 4, 5, 6,7,8,9,10};
int dest[20] = {0};
my_memcpy(dest, src +2, 16);
print_array(dest, size);
- memmove 使用和模拟实现
void *my_memmove(void *dest, const void *src, int num) {
assert(dest != NULL);
assert(src != NULL);
//存储目标对象的起始地址
void *ret = dest;
//目标位置在源位置之前, 由 前 向 后 拷贝
if (dest < src) {
char *d = (char *)dest; // 目标指针
const char *s = (const char *)src; // 源指针
while (num--) {
*d++ = *s++; // 逐字节拷贝,并递增指针
}
} else {
// 如果目标地址在源地址之后,从后往前拷贝
char *d = (char *)dest + num - 1; // 目标指针指向末尾
const char *s = (const char *)src + num - 1; // 源指针指向末尾
while (num--) {
*d-- = *s--; // 逐字节拷贝,并递减指针
}
}
return ret;
}
int src[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
//把 1,2,3,4,5 这几个数字 移动到 第 3,4,5,6,7的位置
my_memmove(src+2, src, 20); //输出 1 2 1 2 3 4 5 8 9 10
my_memmove(src2, src2+2, 20); //输出 3 4 5 6 7 6 7 8 9 10
- typedef
typedef unsigned int uint;
typedef int* pint_t;
//将 parr_t 定义为 typedef int(*)[6] 的别名
typedef int(*parr_t)[6];
void typedef_test() {
int a = 10;
int *pa = &a;
pint_t pb = &a;
printf("pa:%p,pb:%p\n",pa,pb);
int arr[6] = {0};
int (*parr)[6] = &arr; //p 是数组指针
parr_t parr_t = &arr;
printf("parr:%p,parr_t:%p\n",parr,parr_t);
}
- 有趣的代码2
void (*signal(int , void(*)(int)))(int);
使用 typedef 简化后
typedef void(*pf_t2)(int);
pf_t2 signal(int,pf_t2);
- 有趣的代码1
(*(void (*)())0)();
//把 0 地址处,强制转换成函数指针类型 void (*)(), 这个函数指针类型的参数和返回值都是空, 之后对函数指针//解引用 (*(void (*)())0), 再调用 (*(void (*)())0) ()
- 函数指针变量 变量有地址、数组有地址、函数也有地址
int a = 10;
int *pa = &a; //整型指针变量
int arr[5] = {0};
int (*parr)[5] = &arr; //函数指针变量
int(*pf) (int,int) = &add;
//&函数名 和 函数名都是函数地址,没有区别
printf("add %p\n",&add); //函数地址
printf("add %p\n",add);
printf("add %p\n",pf);
- 二维数组传参
void print(int (*p)[5], int r, int c) {
int i = 0;
for (i = 0; i < r; i++) {
int j = 0;
for (j = 0; j < c; j++) {
//p[i] 是 第 i 行的数组名,数组名又表示首元素地址,所以 p[i]表示的是 & p[i][0]
printf("%d ", *(*(p + i) + j)); //*(arr + i) == arr[i]
}
printf("\n");
}
}
int main() {
printf("hello world\n");
int arr[3][5] = {{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}};
print(arr, 3, 5);
}
二维数组传参其实传递的是首元素地址,也就是第一行地址, arr + i 代表跳过 i 行.
- 二维数组 ⼆维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维
数组的每个元素是⼀个⼀维数组。那么⼆维数组的⾸元素就是第⼀⾏,是个⼀维数组。
void printArray(int arr[3][5],int r,int c){
int i = 0;
for (i = 0; i < r;i++){
int j = 0;
for ( j = 0; j < c; ++j) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
*数组指针使用
void array_pointer2(){
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int sz = sizeof(arr) / sizeof(arr[0]);
int *ptr = arr;
for (int i = 0; i < sz;i++){
// printf("%d ",*(ptr+i));
printf("%d ", ptr[i]);
}
printf("\n");
}
- 数组指针变量
指向数组的指针 -> 数组指针变量中存放的是数组的地址
int arr[10] = {1,2,3,4,5};
//p 是数组指针变量, p中存放的是整型数组的地址
int (*ptr) [10] = &arr; //&arr 取出的是数组的地址
char* ch[5];
//pc 是数组指针变量, p中存放的是字符数组的地址
char (*pc)[5] = &ch;
p先和结合,说明p是⼀个指针变量,然后指针指向的是⼀个⼤⼩为10个整型的数组。所以p是 ⼀个指针,指向⼀个数组,叫 数组指针。 这⾥要注意:[]的优先级要⾼于号的,所以必须加上()来保证p先和*结合。
- 字符串比较
- 字符指针
char ch = 'w';
char *pc = &ch;
*pc = 'a';
- 字符串指针
//常量字符串,不能修改,这里的赋值是将字符串中的首字符地址赋给 p
const char *p = "abcdef";
printf("%c\n", *p);
//使用 %s 打印字符串的时候只需要提供首字符的地址就行,这里的 p 就代表首字符地址
printf("%s\n", p);
//%s 不能使用 *p, 因为这里 *p 是代表首字符,字符输出要用 %c, 不是用 %s
// printf("%s\n", *p);
//将收个字符 a 修改为 h,修改失败,因为常量字符串不能修改, 因此 加上 const提醒开发者不要修改
*p = 'e'; //error
printf("%s\n", p);
- 指针数组模拟二维数组
int arr1[5] = {1,2,3,4,5};
int arr2[5] = {2,3,4,5,6};
int arr3[5] = {3,4,5,6,7};
int *arr[] = {arr1,arr2,arr3};
int sz = sizeof(arr) / sizeof(arr[0]);
int arr1_size = sizeof(arr1) / sizeof(arr1[0]);
for (int i = 0; i < sz; i++) {
// printf("%d ", *arr[i]);
for (int j = 0; j < arr1_size; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
- 指针数组
存放指针的数组
int * arr[] //存放整型指针的数组
int * arr[] //存放字符指针的数组
- 二级指针
int a = 10;
int * pa = &a; //pa 是一级指针,指向(存放)的是 a 的地址
int ** ppa = &pa; //ppa 是二级指针,指向(存放)的是 一级指针变量(pa)的地址 后面的 * 代表 ppa 是指针, 前面的 int * 代表 ppa 指向的 是 int *
- 优化冒泡排序
int count = 0;
void bubble_sort(int *arr,int sz) {
int i = 0;
for (i = 0; i < sz; i++) {
//假设已经满足顺序要求
int flag = 1;
for (int j = 0; j < sz - i - 1; j++) {
count++;
if (arr[j] > arr[j + 1]) {
flag = 0;
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
//减少排序次数
if (flag == 1) {
break;
}
}
}
- 数组访问
for (i = 0; i < sz; i++) {
// printf("%d\n",arr[i]); // 方式1
// printf("%d\n",*(p+i)); // 方式2
// printf("%d\n",*(arr+i)); // 方式3
// printf("%d\n",*(i+arr)); // 方式4
printf("%d\n",i[arr]);// 方式5
}
- 数组名是数组首元素地址,但有 2 个例外
1、sizeof (数组名):这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节 2、&数组名: 这里的数组名也表示整个数组,取出的是整个数组的地址 除此之外,任何地方使用数组名,都表示数组的首元素地址
-
传值调用和传址(地址)调用 以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改 主调函数中的变量的值,就需要传址调⽤。
-
strlen 的模拟实现
size_t my_strlen(const char *s) {
assert(s != NULL);
size_t count = 0;
printf("%p\n", s);
while (*s) {
count++;
s++;
printf("count:%d,%p\n", count,s);
}
return count;
}
- 关闭 assert 机制
#define NDEBUG
#include <assert.h>
如果已经确认程序没有问 题,不需要再做断⾔,就在 #include <assert.h> 语句的前⾯,定义⼀个宏 NDEBUG
- 指针赋值为 NULL
NULL 代表地址为 0 的地址,不允许读写
int *p = NULL;
*p = 20; //访问地址为 0的指针,会报错
printf("%d\n", *p);
- 野指针成因
- const 修饰指针变量(放在 * 号前)
const 修饰指针变量,放在 * 的左边,限制的是指针指向的内容,也就是不能通过指针变量来修改它所指向的内容,例如 不能执行 *p = 20, 但是,指针变量本身是可以改变的:p = &m
void const_pointer_test() {
int n = 10;
int m = 100;
const int *p = &n;
printf("const_pointer_test p1:%p\n", p);
printf("p1:%p\n", p);
// *p = 20; //error: 指针变量被 const 修饰,不能通过指针变量来修改它所指向的内容
printf("n = %d\n", n);
p = &m; //ok 指针变量本身是可以改变的
printf("p2:%p\n", p);
}
- const 修饰指针变量(放在 * 号后)
const 修饰指针变量,放在 * 的右边,限制的是指针本身,指针不能改变它的指向,也就是不能执行 p = &m, 但可以通过指针变量修改它所指向的内容,也就是可以执行 *p = 20
void const_pointer_test() {
int n = 10;
int m = 100;
int * const p = &n;
printf("const_pointer_test p1:%p\n", p);
printf("p1:%p\n", p);
*p = 20; //ok: 可以通过指针变量修改它所指向的内容
printf("n = %d\n", n);
// p = &m; //error 指针本身不能改变
printf("p2:%p\n", p);
}
-
void 类型指针 一般 void*类型的指针是使用在函数参数的部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果
-
指针变量的大小
指针变量是用来存放地址的,那么指针变量的大小就得是 4 个字节的空间才可以
- 为什么可以直接修改变量的值,还需要 通过指针去修改
有些时候不方便直接拿到变量,但是可以拿到变量,这样我们就可以通过变量修改值(类似生活中大佬想找某个人不方便出手,可以把这个人的地址交给其他人,其他人通过地址去找到这个人)
- 指针定义
int a = 20;
int * pa = &a;
//将pa 指向的地址存放的值为 30,
*pa = 30;
pa 是变量名字, int * 是变量的类型
其中,每个内存单元,相当于一个学生宿舍,一个字节空间里面能放8个比特位,就好比同学们住的八人间,每个人是一个比特位, 每个内存单元也都有一个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU就可以快速找到一个内存空间, 生活中我们把门牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。C语言中给地址起了新的名字叫:指针。| 所以我们可以理解为: 内存单元的编号 == 地址 == 指针
- 指针类型的意义
1、解引用的权限,1次可以改变多少个字节
int a = 20;
int *pa = &a;
*pa = 0;
//输出 0, 因为 pa 是 int 类型的指针,一次可以修改 4 个字节
printf("a:%d\n",a);
int b = 20;
char *pb = &b;
*pb = 0;
//输出 287453952, 因为 pa 是 char 类型的指针,一次只能修改 1 个字节,因此值为 0x00223344
printf("b:%d\n",b );
2、指针加减整数 指针类型决定了指针向前或向后走一步走多远
- 变量创建的本质其实是在内存中申请空间
int a = 20; 代表向内存申请 4 个字节的空间,用来存放 20 这个数值,这4个字节,每个都有编号(地址)
变量的名字仅仅是给程序员看的 编译器不看名字 编译器是通过!址找内存单テ的T
- 指针理解
int a = 10;
int *pa = &a;
pa 是指针变量的名字,存的是 a 的地址
* 表示 pa 是指针,
int 表示 pa 指向的变量 a 的类型是 int
- 指针变量的大小
32 位机器上占 4 个字节, 64 位机器上占 8 个字节
- 右移操作符
- char 的取值范围
- 统计某个整数中 位 为 1 的个数
/**
* unsigned 的原因是兼容负数的情况
* @param n
* @return 位 为 1 的个数
*/
int count_biy_ones(unsigned int n) {
int count = 0;
while (n) {
if (n % 2 == 1) {
count++;
}
n /= 2;
}
return count;
}
- 导入静态库方式
#pragma comment(lib, "add.lib")
- printf 返回的是打印的字符个数
printf("%d\n", printf("%d",printf("%d",43)));
1、printf("%d",43), 输出 43, 返回值是 2
2、printf("%d",2), 输出 2, 返回值是 1
3、printf("%d",1), 输出 1, 返回值是 1
因此输出 4321.
- 数组传参
数组在作为函数传递的时候,形参和实参的地址是同一个,因此在函数内部修改数组的值,外部传入的实参的数组的值也会被修改, 形参如果是一维数组,数组的大小可以不写
- C99中的变⻓数组
- int 类型打印
有符号的 int, 使用 %d, 无符号的 int 使用 %u, 如果把一个 有符合的 int,例如 -10, 使用 %u 打印,会出现不正确的情况
int n = -100;
printf("n1:%d\n",n); //输出 10
printf("n2:%u\n",n); // 输出 4294967196
-
C语言规定 char 类型默认是否带有正负号,由当前系统决定,这就是说, char 不等同于 signed char,它有可能是 signed char,也有可能是 unsigned char 。这一点与 int 不同,int 就是等同于 signed int 。
-
数据类型最大最小值范围
printf("有符号 char 最大值:%d,最小值:%d\n", SCHAR_MAX,SCHAR_MIN );
printf("无符号 char 最大值:%d\n", UCHAR_MAX);
printf("有符号 short 最大值:%d,最小值:%d\n", SHRT_MAX, SHRT_MIN);
printf("无符号 short 最大值:%d\n", USHRT_MAX);
printf("有符号 int 最大值:%d,最小值:%d\n", INT_MAX, INT_MIN);
printf("无符号 int 最大值:%d\n", UINT_MAX);
printf("有符号 long 最大值:%l,最小值:%l\n", LONG_MAX, LONG_MIN);
printf("无符号 long 最大值:%ld\n", ULONG_MAX);
printf("有符号 long long 最大值:%lld,最小值:%lld\n", LLONG_MAX, LLONG_MIN);
printf("无符号 long 最大值:%lld\n", ULLONG_MAX);
- 变量的存放区域
- 运算
int score;
score = (5 / 20) * 100;
//输出 0
printf("score1:%d\n", score);
//输出 25
score = (5 / 20.0) * 100;
printf("score2:%d\n", score);
- 负数求模的规则
负数求模的规则是,结果的正负号由第一个运算数的正负号决定
printf("%d\n", 11 % -5); //1
printf("%d\n", -11 % -5); //-1
printf("%d\n", -11 % 5); //-1
- 强转类型
double 类型转换成 int 类型, 会将小数点后的数字全部丢掉,没有四舍五入
int a =(int) 3.14;
- 占位符(加粗的需要重点关注)
- 限定宽度
- 总是显示正负号
默认情况下, printf()不对正数显示+号,只对负数显示-号。如果想让正数也输出+号,可以在占位符的 %后面加一个+。
printf("%d\n",12); //输出 12
printf("%d+\n",12); //输出 +12
printf("%d\n",-12); //输出 -12
- 限定小数位数
输出小数时,默认是显示 6 位小数,有时希望限定小数的位数,举例来说,希望小数点后面只保留两位,占位符可以写成%.2f。
printf("%f\n",3.14); //输出 3.140000
printf("%.2f\n",3.14); //输出 3.14
- 使用 *替代最小宽度和小数位数
最小宽度和小数位数这两个限定值,都可以用 * 代替,通过 printf()的参数传入
printf("%*.*f\n",6,2,0.5);
//等价于
printf("%6.2f",0.5);
- 输出部分字符串
%s 占位符用采输出字符串,默认是全部输出。如果只想输出开头的部分,可以用%.[m]s 指定输出的长度,其中[m]代表一个数字,表示所要输出的长度
printf("%.7s\n", "hello world"); //输出 hello w
上面示例中,占位符 %.7s 表示只输出字符串"helo word"的前7个字符
- 使用 scanf 提示警告:
- scanf 的返回值
- scanf 占位符
- scanf 赋值忽略符
- 变量初始化
在创建变量的时候,给变量一个初始值,是一种好的编程习惯如果不给变量初始化,有的编译器会报错。局部变量不初始化的时候,他的值是随机的.全局变量如果没有初始化,默认是 0
- 生成随机数
第 5 章 数组
- 10 进制如何转 2 进制
- 原码、补码、反码
整数的2进制表⽰⽅法有三种,即原码、反码和补码
有符号整数的三种表⽰⽅法均有符号位和数值位两部分,2进制序列中,最⾼位的1位是被当做符号
位,剩余的都是数值位。
符号位都是⽤0表⽰“正”,⽤1表⽰“负”。
正整数的原、反、补码都相同。
负整数的三种表⽰⽅法各不相同。
原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。 补码:反码+1就得到补码。
补码得到原码也是可以使⽤:取反,+1的操作。
对于整形来说:数据存放内存中其实存放的是补码。
为什么呢?
在计算机系统中,数值⼀律⽤补码来表⽰和存储。原因在于,使⽤补码,可以将符号位和数值域统⼀
处理;同时,加法和减法也可以统⼀处理(CPU只有加法器)此外,补码与原码相互转换,其运算
过程是相同的,不需要额外的硬件电路。
- 整数在内存中存的都是补码 (原码取反再加1) 原因是如果用原码相加会出现,整数加负数的结果不正确的情况,以 1 + (- 1) 为例
000000000 000000000 000000000 000000001 //1 的原码
000000000 000000000 000000000 000000001 //1 的补码
100000000 000000000 000000000 000000001 //-1 的原码
111111111 111111111 111111111 111111110 //-1 的反码
111111111 111111111 111111111 111111111 //-1 的补码
//使用原码存储
000000000 000000000 000000000 000000001 // 1 的原码
100000000 000000000 000000000 000000001 //-1 的原码
100000000 000000000 000000000 000000010 //1 -1 得到的值为 -2(最高位为1 代表负数)
//使用补码存储
000000000 000000000 000000000 000000001 // 1 的补码
111111111 111111111 111111111 111111111 //-1 的补码
000000000 000000000 000000000 000000000 //1 -1 得到的值为 0 符合预期
- 原码-反码-补码转换
- 如何在不引入第 3 个变量的条件下,交换两个变量的值
void swap_test() {
int a = 10;
int b = 20;
printf("Before swap: a = %d, b = %d\n", a, b);
a = a + b;
b = a - b;
a = a - b;
printf("After swap: a = %d, b = %d\n", a, b);
}
以上方法如果在比较小的数字可以,如果 a 和 b 都比较大, a +b 可能发生溢出风险
- 交互两个数字的方法
/**
* 引入一个临时变量,可读性高,性能更好
*/
void swap_test1() {
int a = 10;
int b = 20;
printf("swap_test1 Before swap: a = %d, b = %d\n", a, b);
int c = a;
a = b;
b = c;
printf("swap_test1 After swap: a = %d, b = %d\n", a, b);
}
/**
* a 和 b 比较大时,相加有溢出风险
*/
void swap_test2() {
int a = 10;
int b = 20;
printf("swap_test2 Before swap: a = %d, b = %d\n", a, b);
a = a + b;
b = a - b;
a = a - b;
printf("swap_test2 After swap: a = %d, b = %d\n", a, b);
}
/**
* 可读性差,性能不如临时变量的方式
*/
void swap_test3() {
int a = 10;
int b = 20;
printf("swap_test3 Before swap: a = %d, b = %d\n", a, b);
a = a ^ b;
b = a ^ b;
a = a ^ b;
printf("swap_test3 After swap: a = %d, b = %d\n", a, b);
}
- size_t 是一种数据类型,近似于无符号整型,但容量范围一般大于 int 和 unsigned,使用 size_t 是为了保证 arraysize 变量能够有足够大的容量来储存可能大的数组,但凡不涉及负值范围的表示size取值的,都可以用size_t