C语言_结构体_sizeof_分段

结构体

关于;的问题:

  1. 一般来说,大括号{}结尾后不用再加;。但是结构体的定义中的大括号{}后面要加;。因为结构体的定义属于数据的定义,只是有了大括号的形式而已,而数据的定义后面都要加;,因此结构体的定义中的大括号{}后面要加;
  2. 函数、if语句、for语句中的{}是语句的定义,则不用加;
1
2
3
4
5
6
struct Stu
{
unsigned id;
unsigned char age;
char name[10];
};

C语言中,struct结构体名字是一体的,共同才能合成一个类型,不能只写Stu

结构体的定义

1
2
3
4
5
6
7
8
int main()
{
struct Stu stu;
stu.age = 17;
stu.id = 123456;
strcpy(stu.name, "xcg");
printf("Age: %u, ID: %u, Name: %s\n", stu.age, stu.id, stu.name);
}

结构体的大小

1
2
3
4
5
6
7
8
9
struct Test
{
int a; // 4
char b; // 1 --> 实际: 4
};
int main()
{
printf("%u\n", sizeof(struct Test));
}// 8

字节对齐

为了效率,即使char只占用一个字节,也会以结构体中大的数据类型为参考多占一些空间。如此在传输过程中就无需再去一个一个找分量,而是可以直接按照整体处理。一个时钟周期,64位数据都会一次性传输完成,所以说虽然浪费了一些空间,但换来了提高效率。

  1. 每一个元素的偏移量(相对0),都是自身类型大小的整数倍(0也算)。
  2. 结构体的整体大小,是内部最大的基础数据类型大小的整数倍。
  3. 我们控制不了的:结构体的首地址,是体内最大基本数据类型大小的整数倍。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Test
{
int a; // 4
char b; // 1 --> 实际: 2
short c; // 2
};//8
struct Test
{
int a; // 4
char b; // 1 --> 实际: 2
short c; // 2
char d; // 1 --> 实际: 4
};//12
struct Test
{
int a; // 4
char b; // 1
char d; // 1
short c; // 2
short e[10]; // 10 --> 12
};//20

有办法关闭字节对齐特性:#pragma comment package 1

typedef

1
2
3
4
5
6
typedef struct _Stu
{
unsigned id;
unsigned char age;
char name[11];
}Stu;

结构体指针类型

1
2
3
4
5
6
typedef struct _Stu
{
unsigned id;
unsigned char age;
char name[11];
}*PStu;

如此定义,则效果为:PStu相当于Stu*

Stu* stu = (Stu*)malloc(sizeof(Stu));就可以写为

PStu stu = (PStu)malloc(sizeof(Stu));

星号的结合问题

1
typedef int * PMyInt;

如此定义,PMyInt相当于int*,即是一个int指针类型。但是,实际上,*星号是跟着名字PMyInt结合的。int*这种写法只是大多数人的习惯而已。

因此,typedef int MyInt, *PMyInt;才解释得通。如果typedef int * MyInt, PMyInt,则就变成了MyInt是指针类型,而PMyInt变成了int类型!

堆创建结构体

stdlib

C Standard General Utilities Library

malloc

分配指定字节数的内存块,返回一个无类型指针,需要强制转换类型。此空间的伸缩不受限制,因此需要显式分配、释放。

在小型设备上如果内存不够,则有可能会返回NULL。

1
2
3
4
5
#include<stdlib.h>
int main()
{
Stu* stu = (Stu*)malloc(sizeof(Stu));
}

free

1
2
3
4
5
6
7
#include<stdlib.h>
int main()
{
Stu* stu = (Stu*)malloc(sizeof(Stu));
free(stu);
stu = NULL;
}

为什么要置空?不成文的规定:指针是否有效?由指针是否为空决定。如果指针不空则认为是指向的内容有效。

如果不置空,即在释放指向的内容后依旧保留此指针,就成为了无意义的乱指。称为“野指针”或“迷途指针”。

如果反过来:空间内容有效,但指针值丢失,则叫做内存泄漏。

怎么解决?

  1. 小心编程
  2. 关闭进程,让操作系统收回所有内存

结构体指针访问成员

  1. (*stu).age - *优先级是2,.优先级是1
  2. stu->age - arrow运算符。

分段

结构体不仅可以把小类型组合成大类型,还能把基本数据类型拆成小块。

使用“位域”运算符(bit field operator)。

1
2
3
4
5
6
7
8
9
10
struct Test
{
int a : 2;
};
int main()
{
struct Test test;
test.a = 1;
printf("%i\n", test.a);
}// 1

test.a赋值1是可以的。但给test.a赋值2则会打印成-2

1
2
3
4
5
6
int main()
{
struct Test test;
test.a = 2;
printf("%i\n", test.a);
}// -2

因为:a是有符号int型,但只有2位的空间。除去符号位,则只有1位空间。那么,把2这个int字面常量给了a,就相当于:(10)2给了a。那么,a实际空间存的是(10)2,因为它是有符号int,而又因为当前符号位是1,所以实际表达的值为:(10)2的补码,算出无符号值后,再加个负号,即(01)2+1=(10)2(01)_2+1=(10)_2​得出2,再加个负号,即-2

此时的sizeof大小为多少呢?

1
2
3
4
struct Test
{
int a : 2;
};// 4 --> 整个int的大小
1
2
3
4
5
6
//a、b一起分了12个字节,还有20个没分配。因此还是1个int的大小
struct Test
{
int a : 2;
int b : 10;
};// 4 --> 1个int的大小

分隔符

1
2
3
4
5
6
7
8
//a先分了2个字节,但是a剩下的空间不想再用,加个separator分隔符。
//在分隔符后定义b。此时是第2个int空间。
struct Test
{
int a : 2;
int : 0; // separator //无名
int b : 10;
};// 8 --> 2个int的大小

下面这个分隔符的意义在于,虽然实际有效的是int类型,但是long long分隔符的作用在于在第一个和第二个int之间划分了8字节的位置,即a实际被提升为8字节大小,b也被提升为8字节大小。

1
2
3
4
5
6
7
8
//a先分了2个字节,但是a剩下的空间不想再用,加个separator分隔符。
//在分隔符后定义b。此时是第2个int空间。
struct Test
{
int a : 2;
long long : 0; // separator //无名
int b : 10;
};// 16 --> 2个long long的大小

实际用处

  1. 如果小于8 bits,则用位域运算符。如果是8 bits,则直接用char。
  2. separator一般不用,直接占满上面的空位即可,然后紧接着定义下一个类型。

C语言_函数_栈帧_参数_递归

函数

  1. 每个编程语言都有函数。
  2. 函数是代码的模块/容器/载体,代码如果要执行,必须在函数内才能执行。

内存结构、地址空间

  1. 对于可执行文件(Windows下是exe文件;Linux下是out文件),可执行文件有固定格式,从磁盘中加载到内存后,文件中相应部分的代码、数据等会映射到内存的五个不同的模块中。
  2. 每个程序都认为自己独占了整个的地址空间。如果是32位系统,则程序认为独占了32位的内存空间(0到4G)。这就是虚拟地址空间,自从386就开始有这个概念,便于多路程运行。

栈帧

  1. m和n是actual parameters.(实参)
  2. a和b是formal parameters.(形参)
  3. 所有的参数都是按值传递的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
void bar(int a, int b);

int main()
{
int m = 5, n = 6;
bar(m, n);
return 0;
}
void bar(int a, int b)
{
int c = a + b;
printf("%i\n", c);
}

反了?

image-20240316220318599

栈是从高地址向低地址扩展的。越后定义的变量地址越低。

int m = 5, n = 6;这条语句:先定义n,再定义m,因此n在高地址、m在低地址。

printf的参数

variable parameters

1
int printf(char const * const _Format, ...);

启示:对外开放的函数,如果要防止误改内容,则加const表明只读,否则别人不敢传参数。

const是什么

const有三个位置可以放。

看表达式:

  1. 无论右侧是什么,左侧是int,则右侧也是一个int
    1. 左侧一个int,右侧一个a。
1
int a;
  1. 左侧一个int,右侧是*a*a是一个int:表示a这个变量解引用之后则是一个int,则a是一个指向int的指针。
1
int *a;
  1. const是修饰右侧东西的。
    1. 如果在int后写const:*a是一个不能修改的int值。同理const写到int前面也一样:const int * a;
1
int const * a;
  1. 如果在*后写const:a是一个不能修改的值。*a是一个能修改的int值,a这个变量解引用之后则是一个int,则a是一个指向int的指针,但a是一个不能修改的值。

后缀为.c测试

1
2
3
4
5
int main()
{
char * p = "xxx"; //可以编译通过
p[1] = 'M'; //可以编译通过
}

但在运行阶段,会抛出异常:write access violation

不要企图改变常量区。

后缀为.cpp测试

1
2
3
4
5
6
7
int main()
{
char str[] = "Hello";
char * p = str;
p[2] = 'x';
}// 是可以通过p间接修改str[]中的值的。
//最后str[]变为"Hexlo"
1
2
3
4
5
6
int main()
{
char str[] = "Hello";
char * p = str;
p = "xxx";// error //char * p虽然可以改变指针值,但是不能指向常量字符串
}
1
2
3
4
5
6
7
int main()
{
char str[] = "Hello";
const char * p = str;
p = "aaa"; //char * p既可以改变指针值,也可以指向常量字符串
p[2] = 'x'; //error 虽然p可以指向常量字符串,但是不能间接修改值
}
1
2
3
4
5
6
7
int main()
{
char str[] = "Hello";
const char * const p = str;
p = "aaa";//error //char * p不可以改变指针值,也不能通过p间接修改值
p[2] = 'x';//error
}

可变参数

欲用show打印可变参数中第n个值:

需要取函数栈帧中n变量的地址,然后向上寻找n个int大小(因为参数是从高地址到低地址扩展的),即得到可变参数中第n个参数的地址。

栈帧示意图:

image-20240316231327944

1
2
3
4
5
6
7
8
9
10
11
12
void show(int n, ...);
int main()
{
show(2, 10, 20, 30, 40, 50); // 欲打印第2个值,20
}
void show(int n, ...)
{
// 指针进行整数加运算。
printf("%i\n", *(&n + n));
}
//64位下:打印10
//32位下:正确,打印20

奇怪的是,64位下:打印10;32位下:打印20。因为:int固然是4字节大小,而且int的指针加减1的大小也应该是4字节(p + n = p的值 + sizeof(int)* n)。但是,**在64位下的字长是8字节的,因为地址总线每一次至少会传64位(8字节)的内容。因此,每个int实际占用了8个字节。**所以我们p+n仅仅移动了8个字节,只能打到10。

经过调试,可以看到实际的内存内容(16进制):

image-20240316234542234

可以看到,02 0a 14 1e 28 32依次是6个int参数,实际都占用了8个字节。大端地址存放高字节。则如果要打印可变参数中的第n个int,需要*(&n + 2 * n)或者把&n强制转换为64位大小的long long**((long long*)&n + n)

1
2
3
4
5
6
7
8
9
10
11
12
void show(int n, ...);
int main()
{
show(2, 10, 20, 30, 40, 50); // 欲打印第2个值,20
}
void show(int n, ...)
{
// 指针进行整数加运算。
printf("%i\n", *((long long*)&n + n));
}
//64位下:正确,打印20
//32位下:错误,打印286331153

但是,这样的话,虽然64位下打印正确了,但是32位下又错了!因为移动了n个8字节,打到了函数栈帧之外!所以,必须想一个能判断32位、64位的通用方法,去控制int指针的大小(即64位下8字节、32位下4字节)。

Ctrl + 左键点入size_t,会出来一些宏定义:

1
2
3
4
5
6
7
8
#ifdef _WIN64
typedef unsigned __int64 size_t;
typedef __int64 ptrdiff_t;
typedef __int64 intptr_t;
#else
typedef unsigned int size_t;
typedef int ptrdiff_t;
typedef int intptr_t;

在程序为64位编译时,上面三个会生效、下面会失效;32位编译时反之。

通过这个,可以控制int指针的大小。

1
2
3
4
5
6
7
8
9
10
11
12
void show(int n, ...);
int main()
{
show(2, 10, 20, 30, 40, 50); // 欲打印第2个值,20
}
void show(int n, ...)
{
// 指针进行整数加运算。
printf("%i\n", *((intptr_t*)&n + n));
}
//64位下:正确,打印20
//32位下:正确,打印20

数组和函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
void bar(int a[8]);
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
bar(arr);
return 0;
}
// 虽然传的是数组类型,但实际退化为指针了
void bar(int a[8])
{
printf("%i\n", sizeof a);
}
// 64位:8
// 32位:4

虽然形参写的是带元素个数的数组类型,但是因为实际退化为指针了,所以写不写具体数目无所谓:int a[],甚至直接写个int * a也是一样的。如果要告知数组具体个数,需要另传一个int参数n。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
void bar(int * a, int n);
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
bar(arr, sizeof arr / sizeof arr[0]);
return 0;
}
void bar(int * a, int n)
{
for(int i = 0; i < n; ++i)
{
printf("%i\n", a[i]);
}
}// 1 2 3 4 5 6 7 8

二维数组

行信息丢失,需要用int n代替,而我们要保留列信息,才能保证二维数组的有效。

即传一个包含4列元素的行指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void bar(int(*a)[4], int n);
int main()
{
int arr[2][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}};
bar(arr, sizeof arr / sizeof arr[0]);
return 0;
}
void bar(int(*a)[4], int n)
{
for(int i = 0; i < n; ++i, printf("\n");)
{
for(int j = 0; j < 4; ++j)
{
printf("%i\n", a[i][j]);
}
}
}
// 1 2 3 4
// 5 6 7 8

main参数

1
2
3
4
int main(int ac, char * av[])
{
return 0;
}

ac指argument count;av指argument value。

1
2
3
4
5
6
7
8
int main(int ac, char * av[])
{
for(int i = 0; i < ac; ++i)
{
printf("%s\n", av[i]);
}
return 0;
}

编译程序后,在生成目录下命令行(cmd)测试

1
2
3
4
5
6
7
8
9
10
11
C:\Users\xcg\Project1.exe #输入的

C:\Users\xcg\Project1.exe #输出的

C:\Users\xcg\Project1.exe -h -m #输入的

#输出的
C:\Users\xcg\Project1.exe
-h
-m

实质上

实质上,av的类型被退化为了二级指针。

1
2
3
4
5
6
7
8
int main(int ac, char ** av)
{
for(int i = 0; i < ac; ++i)
{
printf("%s\n", *(av + i));
}
return 0;
}

更安全地,加const,让av指向的内容、av的行数组、av本身不可变

1
2
3
4
5
6
7
8
int main(int ac, char const * const * const av)
{
for(int i = 0; i < ac; ++i)
{
printf("%s\n", *(av + i));
}
return 0;
}

递归

递归表现在:行为一致,只是每次数据不一样。

递归的两大要素:递推公式(状态转移方程);终止条件。

  1. 下降的时候执行行为,即行为在调用递归之前,叫首递归
  2. 上升的时候执行行为,即行为在调用递归之后,叫尾递归
  3. 如果递归前后都有行为,叫中间递归
1
2
3
4
5
6
7
8
9
10
11
void show(int n); // show(10) -> 10 9 8 ... 1
int main()
{
show(10);
}
void show(int n)
{
if(n < 1) return;
printf("%i\n", n);
show(n - 1);
}
1
2
3
4
5
6
7
8
9
10
11
void show(int n); // show(10) -> 1 2 3 ... 10
int main()
{
show(10);
}
void show(int n)
{
if(n < 1) return;
show(n - 1);
printf("%i\n", n);
}

求加和

1
2
3
4
5
6
7
8
9
10
int sum(int n); // 1 + 2 + 3 + ... + 10
int main()
{
int r = sum(10);
}
int sum(int n)
{
if(n == 1) return 1;
return n + sum(n - 1);
}// r = 55

Hanoi

1 ~ n的盘子通过A、B、C三个柱子挪到全部C。小的在上面,大的在下面。

1
2
3
4
5
6
7
void hanoi(int n, char from, char via, char to)
{
if(n == 0) return;
hanoi(n - 1, from, to, via);
printf("%d: %c --> %c\n", n, from, to);
hanoi(n - 1, via, from, to);
}

测试

1
2
3
4
int main()
{
hanoi(3, 'A', 'B', 'C');
}
1
2
3
4
5
6
7
1: A --> C
2: A --> B
1: C --> B
3: A --> C
1: B --> A
2: B --> C
1: A --> C