C 语言内置的数据类型,除了最基本的几种原始类型,只有数组属于复合类型,可以同时包含多个值,但是只能包含相同类型的数据,实际使用中并不够用。
实际使用中,主要有下面两种情况,需要更灵活强大的复合类型。
为了解决这些问题,C 语言提供了struct
关键字,允许自定义复合数据类型,将不同类型的值组合在一起。这样不仅为编程提供方便,也有利于增强代码的可读性。
C 语言没有其他语言的对象(object)和类(class)的概念,struct 结构很大程度上提供了对象和类的功能。
struct fraction {
int numerator;
int denominator;
};
/*
定义了新的数据类型以后,就可以声明该类型的变量,这与声明其他类型变量的写法是一样的。
*/
/*
声明自定义类型的变量时,类型名前面,不要忘记加上struct关键字。也就是说,必须使用struct fraction f1声明变量,不能写成fraction f1。
*/
struct fraction f1;
f1.numerator = 22;
f1.denominator = 7;
/*
逐一对属性赋值。上面示例中,先声明了一个struct fraction类型的变量f1,这时编译器就会为f1分配内存,接着就可以通过.运算符为f1的不同属性赋值。
*/
struct car {
char* name;
float price;
int speed;
};
struct car saturn = {"Saturn SL/2", 16000.99, 175};
/*
可以使用大括号,一次性对 struct 结构的所有属性赋值。
*/
/*
变量saturn是struct car类型,大括号里面同时对它的三个属性赋值。如果大括号里面的值的数量,少于属性的数量,那么缺失的属性自动初始化为0。
大括号里面的值的顺序,必须与 struct 类型声明时属性的顺序一致。否则,必须为每个值指定属性名。
struct car saturn = {.speed=172, .name="Saturn SL/2"};
*/
struct car saturn = {.speed=172, .name="Saturn SL/2"};
saturn.speed = 168;
/*
声明变量以后,可以修改某个属性的值。
*/
struct book {
char title[500];
char author[100];
float value;
} b1 = {"title", "Author", 56.7};
/*
struct 的数据类型声明语句与变量的声明语句,可以合并为一个语句。
*/
struct {
char title[500];
char author[100];
float value;
} b1;
/*
该语句同时声明了数据类型book和该类型的变量b1。如果类型标识符book只用在这一个地方,后面不再用到,这里可以将类型名省略。(什么是只用在一个地方???)
*/
struct {
char title[500];
char author[100];
float value;
} b1 = {"Harry Potter", "J. K. Rowling", 10.0},
b2 = {"Cancer Ward", "Aleksandr Solzhenitsyn", 7.85};
/*
`struct`声明了一个匿名数据类型,然后又声明了这个类型的变量`b1`。
与其他变量声明语句一样,可以在声明变量的同时,对变量赋值。
*/
// 指针变量也可以指向struct结构。
struct book {
char title[500];
char author[100];
float value;
}* b1;
// 或者写成两个语句
struct book {
char title[500];
char author[100];
float value;
};
struct book* b1;
/*
变量b1是一个指针,指向的数据是struct book类型的实例。
*/
// struct 结构也可以作为数组成员。
struct title numbers[1000];
numbers[0].numerator = 22;
numbers[0].denominator = 7;
struct 结构占用的存储空间,不是各个属性存储空间的总和,而是最大内存占用属性的存储空间的倍数,其他属性会添加空位与之对齐。
为什么浪费这么多空间进行内存对齐呢?这是为了加快读写速度,把内存占用划分成等长的区块,就可以快速在 struct 结构体中定位到每个属性的起始地址。
由于这个特性,在有必要的情况下,定义 struct 结构体时,可以采用存储大小递增(或递减)的顺序,定义每个属性,这样就能节省一些空间。
struct foo {
int a; // 4 -> 8 空4个
char* b; // 8
char c; // 1 -> 8 空7个
};
printf("%d\n", sizeof(struct foo)); // 24
struct foo {
char c; // 1 -> 4 空三个
int a; // 4
char* b; // 8
};
printf("%d\\n", sizeof(struct foo)); // 16
上面示例中,占用空间最小的char c
排在第一位,其次是int a
,占用空间最大的char* b
排在最后。整个strct foo
的内存占用就从24字节下降到16字节。
第一种方法,struct 变量也类似于其他变量,可以使用赋值运算符(=
),赋值给另一个变量。
struct cat { char name[30]; short age; } a, b;
strcpy(a.name, "Hello");
a.age = 3;
b = a; // struct赋值
b.name[0] = 'M';
printf("%s\n", a.name); // Hello
printf("%s\n", b.name); // Mello
注意:
=
赋值要求两个变量是同一个类型,不同类型的 struct 变量无法互相赋值。==
和!=
)比较两个数据结构是否相等或不等第二种方法,可以使用函数memcpy来为结构体赋值,将一个结构体的指定长度的内存区域复制到另一个结构体的指定位置
// 定义两个学生结构体
Student student1 = { 1001, "Alice", 90.5 };
Student student2;
// 为 student2 赋值
memcpy(&student2, &student1, sizeof(Student));
struct类型的指针和其他数据类型的指针是相似的,完全可以按照其他指针的理解方式来学习struct指针。
传入struct变量,函数内部得到的是一个原始值的副本。
#include
struct turtle {
char* name;
char* species;
int age;
};
void fun(struct turtle t) {
t.age = t.age + 1;
}
int main() {
struct turtle myTurtle = {"Turtle", "sea turtle", 18};
fun(myTurtle);
printf("Age is %i\\n", myTurtle.age); // 输出 18
return 0;
}
上面示例中,函数happy()
传入的是一个 struct 变量myTurtle
,函数内部有一个自增操作。但是,执行完happy()
以后,函数外部的age
属性值根本没变。原因就是函数内部得到的是 struct 变量的副本,改变副本影响不到函数外部的原始数据。
通常情况下,开发者希望传入函数的是同一份数据,函数内部修改数据以后,会反映在函数外部。而且,传入的是同一份数据,也有利于提高程序性能。这时就需要将 struct 变量的指针传入函数,通过指针来修改 struct 属性,就可以影响到函数外部。
传入struct指针,那么通过指针修改数据就会改变之前保存的数据(不再是副本)
void happy(struct turtle* t) {...}
happy(&myTurtle);
上面代码中,t
是 struct 结构的指针,调用函数时传入的是指针。struct 类型跟数组不一样,类型标识符本身并不是指针,所以传入时,指针必须写成&myTurtle
。
函数内部也必须使用(*t).age
或t->age
的写法从指针拿到 struct 结构本身。
void happy(struct turtle* t) {
(*t).age = (*t).age + 1;
// t->age = t->age + 1
// 大大增强了代码的可读性
}
上面示例中,(*t).age
不能写成*t.age
,因为点运算符.
的优先级高于*
。*t.age
这种写法会将t.age
看成一个指针,然后取它对应的值,会出现无法预料的结果。
现在,重新编译执行上面的整个示例,happy()
内部对 struct 结构的操作,就会反映到函数外部。
这里提及一个在链表中容易犯错的知识:
move = l1;
和 move->next = l1;
是不完全等价的。
move = l1;
是将指针 move
直接指向 l1
所指向的节点。这意味着 move
和 l1
同时指向同一个节点,它们指向的是同一块内存地址。move->next = l1;
是将 move
指针所指向节点的 next
指针指向 l1
所指向的节点。这意味着将 l1
插入到 move
所指向节点的后面。如果只执行 move = l1;
,则 l1
并没有插入到新链表中,而是直接将 move
指向了 l1
所指向的节点。
如果只执行 move->next = l1;
,则相当于将 l1
插入到了 move
所指向的节点的后面,但没有将 move
指针移动到新的节点。
struct 结构的成员可以是另一个 struct 结构。
struct species {
char* name;
int kinds;
};
struct fish {
char* name;
int age;
struct species breed;
};
// fish的属性breed是另一个struct结构species。
赋值的时候有多种写法。
// 写法一
struct fish shark = {"shark", 9, {"Selachimorpha", 500}};
// 写法二
struct species myBreed = {"Selachimorpha", 500};
struct fish shark = {"shark", 9, myBreed};
// 写法三
struct fish shark = {
.name="shark",
.age=9,
.breed={"Selachimorpha", 500}
};
// 写法四
struct fish shark = {
.name="shark",
.age=9,
.breed.name="Selachimorpha",
.breed.kinds=500
// 引用breed属性的内部属性,要使用两次点运算符
};
printf("Shark's species is %s", shark.breed.name);
struct name {
char first[50];
char last[50];
};
struct student {
struct name name;
short age;
char sex;
} student1;
/*
对字符数组属性赋值,要使用strcpy()函数,不能直接赋值,因为直接改掉字符数组名的地址会报错。
*/
strcpy(student1.name.first, "Harry");
strcpy(student1.name.last, "Potter");
// or
struct name myname = {"Harry", "Potter"};
student1.name = myname;
struct 结构内部不仅可以引用其他结构,还可以自我引用,即结构内部引用当前结构。比如,链表结构的节点就可以写成下面这样。
struct node {
int data;
struct node* next;
};
struct node {
int data;
struct node* next;
};
struct node* head;
// 生成一个三个节点的列表 (11)->(22)->(33)
head = malloc(sizeof(struct node));
head->data = 11;
head->next = malloc(sizeof(struct node));
head->next->data = 22;
head->next->next = malloc(sizeof(struct node));
head->next->next->data = 33;
head->next->next->next = NULL;
// 遍历这个列表
for (struct node *cur = head; cur != NULL; cur = cur->next) {
printf("%d\\n", cur->data);
}
typedef type name;
为某个类型起别名
type代表类型名,name代表别名。
typedef unsigned char BYTE;
BYTE c = 'z';
typedef 可以一次指定多个别名。
~~typedef int antelope, bagel, mushroom;~~
typedef 可以为指针起别名。
typedef int* intptr;
int a = 10;
intptr x = &a;
/*
使用的时候要小心,这样不容易看出来,变量x是一个指针类型。
*/
typedef 也可以用来为数组类型起别名。
typedef int five_ints[5];
five_ints x = {11, 22, 33, 44, 55};
typedef 为函数起别名的写法如下。
typedef signed char (*fp)(void);
/*
类型别名fp是一个指针,代表函数signed char (*)(void)。
*/
typedef为结构体起别名
typedef struct cell_phone {
int cell_no;
float minutes_of_charge;
} phone;
// 或者把typedef带下来,typedef struct cell_phone phone
// 还可以省略结构体的名字
phone p = {5551234, 5};
/*
typedef命令可以为 struct 结构指定一个别名,
这样使用起来更简洁。phone是别名,而不是结构体变量!
*/
(1)更好的代码可读性。
(2)为 struct、union、enum 等命令定义的复杂数据结构创建别名,从而便于引用。
(3)typedef 方便以后为变量改类型。
typedef float app_float;
app_float f1, f2, f3;
/*
变量f1、f2、f3的类型都是float。如果以后需要为它们改类型,只需要修改typedef语句即可。
*/
(4)可移植性
某一个值在不同计算机上的类型,可能是不一样的。
int i = 100000;
上面代码在32位整数的计算机没有问题,但是在16位整数的计算机就会出错。
C 语言的解决办法,就是提供了类型别名,在不同计算机上会解释成不同类型,比如int32_t
。
int32_t i = 100000;
上面示例将变量i
声明成int32_t
类型,保证它在不同计算机上都是32位宽度,移植代码时就不会出错。
这一类的类型别名都是用 typedef 定义的。下面是类似的例子。
typedef long int ptrdiff_t;
typedef unsigned long int size_t;
typedef int wchar_t;
这些整数类型别名都放在头文件stdint.h
,不同架构的计算机只需修改这个头文件即可,而无需修改代码。
因此,typedef
有助于提高代码的可移植性,使其能适配不同架构的计算机。
(5)简化类型声明
C 语言有些类型声明相当复杂,比如下面这个。
char (*(*x(void))[5])(void);
typedef 可以简化复杂的类型声明,使其更容易理解。首先,最外面一层起一个类型别名。
typedef char (*Func)(void);
Func (*x(void))[5];
这个看起来还是有点复杂,就为里面一层也定义一个别名。
typedef char (*Func)(void);
typedef Func Arr[5];
Arr* x(void);
上面代码就比较容易解读了。
x
是一个函数,返回一个指向 Arr 类型的指针。Arr
是一个数组,有5个成员,每个成员是Func
类型。Func
是一个函数指针,指向一个无参数、返回字符值的函数。C 语言的内存管理,分成两部分。
一部分是系统管理的
系统管理的内存,主要是函数内部的变量(局部变量)。这部分变量在函数运行时进入内存,函数运行结束后自动从内存卸载。这些变量存放的区域称为”栈“(stack),”栈“所在的内存是系统自动管理的。
另一部分是用户手动管理的
用户手动管理的内存,主要是程序运行的整个过程中都存在的变量(全局变量),这些变量需要用户手动从内存释放。如果使用后忘记释放,它就一直占用内存,直到程序退出,这种情况称为**”内存泄漏“(memory leak)。**这些变量所在的内存称为”堆“(heap),”堆“所在的内存是用户手动管理的。
功能:手动分配内存,系统就会在“堆”中分配一段连续的内存;动态数组
函数原型:void *malloc(size_t size)
参数:size是指分配的字节数
返回值:是一个指向分配内存首地址的指针;若分配失败,则返回NULL
注意:
malloc分配内存并不会进行初始化,所以内存中还保留着之前的值
有时候为了增加代码的可读性,可以对malloc()返回的指针进行一次强制类型转换。
一定要注意检查malloc是否分配失败!
/* malloc()最常用的场合,就是为数组和自定义数据结构分配内存。 */
int *p = (int *) malloc(sizeof(int) * 10);
for (int i = 0; i < 10; i++)
p[i] = i * 5;
/* malloc()用来创建数组,有一个好处,就是它可以创建动态数组,即根据成员数量的不同,而创建长度不同的数组。 */
int *p = (int *) malloc(n * sizeof(int));
/*
注意,malloc()不会对所分配的内存进行初始化,里面还保存着原来的值。如果没有初始化,就使用这段内存,可能从里面读到以前的值。程序员要自己负责初始化,比如,字符串初始化可以使用strcpy()函数。
*/
char *p = malloc(sizeof(char) * 4);
strcpy(p, "abc");
// or
p = "abc";
功能:手动分配内存空间,并将内存存放的值初始化为0
函数原型:void* calloc(size_t n, size_t size);
返回值:一个指向分配内存首地址的指针;分配失败时,返回 NULL
参数:第一个参数是某种数据类型的值的数量;第二个是该数据类型的字节长度。分配内存的大小是由这两者的乘积决定。
注意:
malloc() 不会对内存进行初始化,如果想要初始化为0
,还要额外调用memset()
函数。
int* p = calloc(10, sizeof(int));
// 等同于
int* p = malloc(sizeof(int) * 10);
memset(p, 0, sizeof(int) * 10);
功能:realloc()
函数用于修改已经分配的内存块的大小,可以放大也可以缩小
函数原型:void* realloc(void* block, size_t size)
返回值:返回一个指向新的内存块的指针。如果分配不成功,返回 NULL。
参数:
block
:已经分配好的内存块指针(由malloc()
或calloc()
或realloc()
产生)。size
:该内存块的新大小,单位为字节。注意:
realloc()
可能返回一个全新的地址(数据也会自动复制过去),也可能返回跟原来一样的地址。
realloc()
优先在原有内存块上进行缩减,尽量不移动数据,所以通常是返回原先的地址。如果新内存块小于原来的大小,则丢弃超出的部分;如果大于原来的大小,则不对新增的部分进行初始化(程序员可以自动调用memset()
)。
realloc()
的第一个参数可以是 NULL,这时就相当于新建一个指针。
char* p = realloc(NULL, 3490); // 等同于 char* p = malloc(3490);
如果realloc()
的第二个参数是0
,就会释放掉内存块。
由于有分配失败的可能,所以调用realloc()
以后,最好检查一下它的返回值是否为 NULL。分配失败时,原有内存块中的数据不会发生改变。
float* new_p = realloc(p, sizeof(*p * 40));
if (new_p == NULL) {
printf("Error reallocing\\n");
return 1;
}
realloc()
不会对内存块进行初始化。
功能:释放手动分配的内存
函数原型:void free(void *block)
参数:指向开辟内存的指针block
返回值:无返回值
注意:
若手动分配的内存不释放掉,那这个内存块会一直占用到程序运行结束。
// free()的参数是malloc()返回的内存地址
// 分配内存后,若不再使用,一定要及时释放,否则容易造成内存泄露!
int* p = (int*) malloc(sizeof(int));
*p = 12;
free(p);
分配的内存块一旦释放,就不应该再次操作已经释放的地址,也不应该再次使用free()对该地址释放第二次。
声明指针变量时,可以使用restrict
说明符,告诉编译器,该块内存区域只有当前指针一种访问方式,其他指针不能读写该块内存。这种指针称为“受限指针”(restrict pointer)。
int *restrict p;
p = malloc(sizeof(int));
上面示例中,声明指针变量p
时,加入了restrict
说明符,使得p
变成了受限指针。后面,当p
指向malloc()
函数返回的一块内存区域,就味着,该区域只有通过p
来访问,不存在其他访问方式。
int* restrict p;
p = malloc(sizeof(int));
int* q = p;
*q = 0; // 未定义行为
上面示例中,另一个指针q
与受限指针p
指向同一块内存,现在该内存有p
和q
两种访问方式。这就违反了对编译器的承诺,后面通过*q
对该内存区域赋值,会导致未定义行为。
功能:memcpy()
用于将一块源地址内存拷贝到另一块目标地址内存中,**不能有重叠内存区域。**该函数的原型定义在头文件string.h
。
函数原型:void *memcpy(void *restrict dest, void *restrict source, size_t n);
参数:dest
是目标地址,source
是源地址,第三个参数n
是要拷贝的字节数n
返回值:memcpy()
的返回值是第一个参数,即目标地址的指针。
注意:
**dest
和source
都是 void 指针,表示可以移动任何类型的内存数据**n
就等于10 * sizeof(double)
,而不是10
。该函数会将从source
开始的n
个字节,拷贝到dest
。**dest
和source
都是 void 指针,表示这里不限制指针类型,各种类型的内存数据都可以拷贝**memcpy()
只是将一段内存的值,复制到另一段内存,所以不需要知道内存里面的数据是什么类型。复制字符串
/*
`memcpy()`可以取代`strcpy()`进行字符串拷贝,而且是更好的方法,不仅更安全,速度也更快,它不检查字符串尾部的`\0`字符。
*/
#include
#include
int main(void) {
char s[] = "Goats!";
char t[100];
memcpy(t, s, sizeof(s)); // 拷贝7个字节,包括终止符
printf("%s\n", t); // "Goats!"
return 0;
}
my_memcpy.c
void* my_memcpy(void* dest, void* src, int byte_count) {
char* s = src;
char* d = dest;
while (byte_count--) {
*d++ = *s++;
}
return dest;
}
/*
不管传入的`dest`和`src`是什么类型的指针,将它们重新定义成一字节的 char 指针,这样就可以逐字节进行复制。`*d++ = *s++`语句相当于先执行`*d = *s`(源字节的值复制给目标字节),然后各自移动到下一个字节。最后,返回复制后的`dest`指针,便于后续使用。
*/
功能:memmove()
函数用于将一段源地址内存数据复制到另一段目标地址内存中。允许dest和source所指向的内存区域有重叠。该函数的原型定义在头文件string.h
。
函数原型:void *memmove(void *dest, void *source, size_t n);
参数:dest
是目标地址,source
是源地址,n
是要移动的字节数。
返回值:memmove()
返回值是第一个参数,即目标地址的指针。
注意:
如果dest和source所指向的内存区域发生重叠,源区域的内容会被更改;如果没有重叠,它与memcpy()
行为相同。
char x[] = "Hello world!";
// 输出 world!world!
printf("%s\n", (char *) memmove(x, &x[6], 6));
上面示例中,从字符串x
的6号位置开始6个字节,就是“world!”,memmove()
将其前移到0号位置,所以x
就变成了“world!world!”。
功能:memcmp()
函数用来比较两个内存区域。它的原型定义在string.h
。
函数原型:int memcmp(const void *s1, const void *s2, size_t n);
参数:前两个参数是用来比较的指针,第三个参数指定比较的字节数。
返回值:两块内存区域的每个字节以字符形式解读,按照字典顺序进行比较(从左到右比较ASCII码值),如果两者相同,返回0
;如果s1
大于s2
,返回大于0的整数;如果s1
小于s2
,返回小于0的整数。
注意:
与strncmp()
不同,memcmp()
可以比较内部带有字符串终止符\0
的内存区域。
char s1[] = {'b', 'i', 'g', '\0', 'c', 'a', 'r'};
char s2[] = {'b', 'i', 'g', '\0', 'c', 'a', 't'};
if (memcmp(s1, s2, 3) == 0) // true
if (memcmp(s1, s2, 4) == 0) // true
if (memcmp(s1, s2, 7) == 0) // false
if (strncmp(s1, s2, 3) == 0) // true
if (strncmp(s1, s2, 7) == 0) // true 注意strncmp不会比较'\0'之后的内容
功能:在内存区域中查找指定字符。
函数原型:void *memchr(const void *s, int c, size_t n);
参数:第一个参数是一个指针,指向内存区域的开始位置;第二个参数是所要查找的字符,第三个参数是待查找内存区域的字节长度。
返回值:一旦找到,它就会停止查找,并返回指向该位置的指针。如果直到检查完指定的字节数,依然没有发现指定字符,则返回 NULL。
功能:将一段内存全部设置为指定值。
函数原型:void *memset(void *s, int c, size_t n);
参数:第一个参数是一个指针,指向内存区域的开始位置;第二个参数是待写入的字符值;第三个参数是一个整数,表示需要待格式化的字节数。
返回值:返回第一个参数(指针)。
简单例子
char str[] = "BBBBBBBBBBBB";
// 输出 bbbbbbbBBBBB
printf("%s\n", (char*) memset(str, 'b', 7));
注意:
malloc开辟内存后没有初始化,memset()
可以将开辟内存区域全部初始化为0。
int *arr = (int *)malloc(100 * sizeof(int));
memset(arr, 0, 100 * sizeof(int)); // 第三个参数不是sizeof(arr)
// ...
free(arr);