首页
前端面试题
前端报错总结
电子书
更多
插件下载
Search
1
JavaScript基础(二)操作符 流程控制
42 阅读
2
HTML基础
20 阅读
3
Vue基础
17 阅读
4
wctype.h
14 阅读
5
Vue2(知识点)
13 阅读
默认分类
HTML CSS
HTML基础
CSS
HTML5 CSS3
javaScript
javaScript基础
javaScript高级
Web APIs
jQuery
js小总结
WEB开发布局
Vue
PS切图
数据可视化
Git使用
Uniapp
c语言入门
标准库
嵌入式
登录
Search
liuxiaobai
累计撰写
108
篇文章
累计收到
12
条评论
首页
栏目
默认分类
HTML CSS
HTML基础
CSS
HTML5 CSS3
javaScript
javaScript基础
javaScript高级
Web APIs
jQuery
js小总结
WEB开发布局
Vue
PS切图
数据可视化
Git使用
Uniapp
c语言入门
标准库
嵌入式
页面
前端面试题
前端报错总结
电子书
插件下载
搜索到
41
篇与
的结果
2023-09-20
struct 结构(结构体)
struct 结构简介C 语言内置的数据类型,除了最基本的几种原始类型,只有数组属于复合类型,可以同时包含多个值,但是只能包含相同类型的数据,实际使用中并不够用。实际使用中,主要有下面两种情况,需要更灵活强大的复合类型。复杂的物体需要使用多个变量描述,这些变量都是相关的,最好有某种机制将它们联系起来。某些函数需要传入多个参数,如果一个个按照顺序传入,非常麻烦,最好能组合成一个复合结构传入。为了解决这些问题,C 语言提供了struct关键字,允许自定义复合数据类型,将不同类型的值组合在一起。这样不仅为编程提供方便,也有利于增强代码的可读性。C 语言没有其他语言的对象(object)和类(class)的概念,struct 结构很大程度上提供了对象和类的功能。下面是struct自定义数据类型的一个例子。struct fraction { int numerator; int denominator; };上面示例定义了一个分数的数据类型struct fraction,包含两个属性numerator和denominator。注意,作为一个自定义的数据类型,它的类型名要包括struct关键字,比如上例是struct fraction,单独的fraction没有任何意义,甚至脚本还可以另外定义名为fraction的变量,虽然这样很容易造成混淆。另外,struct语句结尾的分号不能省略,否则很容易产生错误。定义了新的数据类型以后,就可以声明该类型的变量,这与声明其他类型变量的写法是一样的。struct fraction f1; f1.numerator = 22; f1.denominator = 7;上面示例中,先声明了一个struct fraction类型的变量f1,这时编译器就会为f1分配内存,接着就可以为f1的不同属性赋值。可以看到,struct 结构的属性通过点(.)来表示,比如numerator属性要写成f1.numerator。再提醒一下,声明自定义类型的变量时,类型名前面,不要忘记加上struct关键字。也就是说,必须使用struct fraction f1声明变量,不能写成fraction f1。除了逐一对属性赋值,也可以使用大括号,一次性对 struct 结构的所有属性赋值。struct car { char* name; float price; int speed; }; struct car saturn = {"Saturn SL/2", 16000.99, 175};上面示例中,变量saturn是struct car类型,大括号里面同时对它的三个属性赋值。如果大括号里面的值的数量,少于属性的数量,那么缺失的属性自动初始化为0。注意,大括号里面的值的顺序,必须与 struct 类型声明时属性的顺序一致。否则,必须为每个值指定属性名。struct car saturn = {.speed=172, .name="Saturn SL/2"};上面示例中,初始化的属性少于声明时的属性,这时剩下的那些属性都会初始化为0。声明变量以后,可以修改某个属性的值。struct car saturn = {.speed=172, .name="Saturn SL/2"}; saturn.speed = 168;上面示例将speed属性的值改成168。struct 的数据类型声明语句与变量的声明语句,可以合并为一个语句。struct book { char title[500]; char author[100]; float value; } b1;上面的语句同时声明了数据类型book和该类型的变量b1。如果类型标识符book只用在这一个地方,后面不再用到,这里可以将类型名省略。struct { char title[500]; char author[100]; float value; } b1;上面示例中,struct声明了一个匿名数据类型,然后又声明了这个类型的变量b1。与其他变量声明语句一样,可以在声明变量的同时,对变量赋值。struct { char title[500]; char author[100]; float value; } b1 = {"Harry Potter", "J. K. Rowling", 10.0}, b2 = {"Cancer Ward", "Aleksandr Solzhenitsyn", 7.85};上面示例中,在声明变量b1和b2的同时,为它们赋值。下一章介绍的typedef命令可以为 struct 结构指定一个别名,这样使用起来更简洁。typedef struct cell_phone { int cell_no; float minutes_of_charge; } phone; phone p = {5551234, 5};上面示例中,phone就是struct cell_phone的别名。指针变量也可以指向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 fraction numbers[1000]; numbers[0].numerator = 22; numbers[0].denominator = 7;上面示例声明了一个有1000个成员的数组numbers,每个成员都是自定义类型fraction的实例。struct 结构占用的存储空间,不是各个属性存储空间的总和,而是最大内存占用属性的存储空间的倍数,其他属性会添加空位与之对齐。这样可以提高读写效率。struct foo { int a; char* b; char c; }; printf("%d\n", sizeof(struct foo)); // 24上面示例中,struct foo有三个属性,在64位计算机上占用的存储空间分别是:int a占4个字节,指针char* b占8个字节,char c占1个字节。它们加起来,一共是13个字节(4 + 8 + 1)。但是实际上,struct foo会占用24个字节,原因是它最大的内存占用属性是char* b的8个字节,导致其他属性的存储空间也是8个字节,这样才可以对齐,导致整个struct foo就是24个字节(8 * 3)。多出来的存储空间,都采用空位填充,所以上面的`structfoo`真实的结构其实是下面这样。struct foo { int a; // 4 char pad1[4]; // 填充4字节 char *b; // 8 char c; // 1 char pad2[7]; // 填充7字节 }; printf("%d\n", sizeof(struct foo)); // 24为什么浪费这么多空间进行内存对齐呢?这是为了加快读写速度,把内存占用划分成等长的区块,就可以快速在 Struct 结构体中定位到每个属性的起始地址。由于这个特性,在有必要的情况下,定义 Struct 结构体时,可以采用存储空间递增的顺序,定义每个属性,这样就能节省一些空间。struct foo { char c; int a; char* b; }; printf("%d\n", sizeof(struct foo)); // 16上面示例中,占用空间最小的char c排在第一位,其次是int a,占用空间最大的char* b排在最后。整个strct foo的内存占用就从24字节下降到16字节。struct 的复制struct 变量可以使用赋值运算符(=),复制给另一个变量,这时会生成一个全新的副本。系统会分配一块新的内存空间,大小与原来的变量相同,把每个属性都复制过去,即原样生成了一份数据。这一点跟数组的复制不一样,务必小心。struct cat { char name[30]; short age; } a, b; strcpy(a.name, "Hula"); a.age = 3; b = a; b.name[0] = 'M'; printf("%s\n", a.name); // Hula printf("%s\n", b.name); // Mula上面示例中,变量b是变量a的副本,两个变量的值是各自独立的,修改掉b.name不影响a.name。上面这个示例是有前提的,就是 struct 结构的属性必须定义成字符数组,才能复制数据。如果稍作修改,属性定义成字符指针,结果就不一样。struct cat { char* name; short age; } a, b; a.name = "Hula"; a.age = 3; b = a;上面示例中,name属性变成了一个字符指针,这时a赋值给b,导致b.name也是同样的字符指针,指向同一个地址,也就是说两个属性共享同一个地址。因为这时,struct 结构内部保存的是一个指针,而不是上一个例子的数组,这时复制的就不是字符串本身,而是它的指针。并且,这个时候也没法修改字符串,因为字符指针指向的字符串是不能修改的。总结一下,赋值运算符(=)可以将 struct 结构每个属性的值,一模一样复制一份,拷贝给另一个 struct 变量。这一点跟数组完全不同,使用赋值运算符复制数组,不会复制数据,只会共享地址。注意,这种赋值要求两个变量是同一个类型,不同类型的 struct 变量无法互相赋值。另外,C 语言没有提供比较两个自定义数据结构是否相等的方法,无法用比较运算符(比如==和!=)比较两个数据结构是否相等或不等。struct 指针如果将 struct 变量传入函数,函数内部得到的是一个原始值的副本。#include <stdio.h> struct turtle { char* name; char* species; int age; }; void happy(struct turtle t) { t.age = t.age + 1; } int main() { struct turtle myTurtle = {"MyTurtle", "sea turtle", 99}; happy(myTurtle); printf("Age is %i\n", myTurtle.age); // 输出 99 return 0; }上面示例中,函数happy()传入的是一个 struct 变量myTurtle,函数内部有一个自增操作。但是,执行完happy()以后,函数外部的age属性值根本没变。原因就是函数内部得到的是 struct 变量的副本,改变副本影响不到函数外部的原始数据。通常情况下,开发者希望传入函数的是同一份数据,函数内部修改数据以后,会反映在函数外部。而且,传入的是同一份数据,也有利于提高程序性能。这时就需要将 struct 变量的指针传入函数,通过指针来修改 struct 属性,就可以影响到函数外部。struct 指针传入函数的写法如下。void happy(struct turtle* t) { } happy(&myTurtle);上面代码中,t是 struct 结构的指针,调用函数时传入的是指针。struct 类型跟数组不一样,类型标识符本身并不是指针,所以传入时,指针必须写成&myTurtle。函数内部也必须使用(*t).age的写法,从指针拿到 struct 结构本身。void happy(struct turtle* t) { (*t).age = (*t).age + 1; }上面示例中,(*t).age不能写成*t.age,因为点运算符.的优先级高于*。*t.age这种写法会将t.age看成一个指针,然后取它对应的值,会出现无法预料的结果。现在,重新编译执行上面的整个示例,happy()内部对 struct 结构的操作,就会反映到函数外部。(*t).age这样的写法很麻烦。C 语言就引入了一个新的箭头运算符(->),可以从 struct 指针上直接获取属性,大大增强了代码的可读性。void happy(struct turtle* t) { t->age = t->age + 1; }总结一下,对于 struct 变量名,使用点运算符(.)获取属性;对于 struct 变量指针,使用箭头运算符(->)获取属性。以变量myStruct为例,假设ptr是它的指针,那么下面三种写法是同一回事。// ptr == &myStruct myStruct.prop == (*ptr).prop == ptr->propstruct 的嵌套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 }; printf("Shark's species is %s", shark.breed.name);上面示例展示了嵌套 Struct 结构的四种赋值写法。另外,引用breed属性的内部属性,要使用两次点运算符(shark.breed.name)。下面是另一个嵌套 struct 的例子。struct name { char first[50]; char last[50]; }; struct student { struct name name; short age; char sex; } student1; strcpy(student1.name.first, "Harry"); strcpy(student1.name.last, "Potter"); // or struct name myname = {"Harry", "Potter"}; student1.name = myname;上面示例中,自定义类型student的name属性是另一个自定义类型,如果要引用后者的属性,就必须使用两个.运算符,比如student1.name.first。另外,对字符数组属性赋值,要使用strcpy()函数,不能直接赋值,因为直接改掉字符数组名的地址会报错。struct 结构内部不仅可以引用其他结构,还可以自我引用,即结构内部引用当前结构。比如,链表结构的节点就可以写成下面这样。struct node { int data; struct node* next; };上面示例中,node结构的next属性,就是指向另一个node实例的指针。下面,使用这个结构自定义一个数据链表。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); }上面示例是链表结构的最简单实现,通过for循环可以对其进行遍历。位字段struct 还可以用来定义二进制位组成的数据结构,称为“位字段”(bit field),这对于操作底层的二进制数据非常有用。struct { unsigned int ab:1; unsigned int cd:1; unsigned int ef:1; unsigned int gh:1; } synth; synth.ab = 0; synth.cd = 1;上面示例中,每个属性后面的:1,表示指定这些属性只占用一个二进制位,所以这个数据结构一共是4个二进制位。注意,定义二进制位时,结构内部的各个属性只能是整数类型。实际存储的时候,C 语言会按照int类型占用的字节数,存储一个位字段结构。如果有剩余的二进制位,可以使用未命名属性,填满那些位。也可以使用宽度为0的属性,表示占满当前字节剩余的二进制位,迫使下一个属性存储在下一个字节。struct { unsigned int field1 : 1; unsigned int : 2; unsigned int field2 : 1; unsigned int : 0; unsigned int field3 : 1; } stuff;上面示例中,stuff.field1与stuff.field2之间,有一个宽度为两个二进制位的未命名属性。stuff.field3将存储在下一个字节。弹性数组成员很多时候,不能事先确定数组到底有多少个成员。如果声明数组的时候,事先给出一个很大的成员数,就会很浪费空间。C 语言提供了一个解决方法,叫做弹性数组成员(flexible array member)。如果不能事先确定数组成员的数量时,可以定义一个 struct 结构。struct vstring { int len; char chars[]; };上面示例中,struct vstring结构有两个属性。len属性用来记录数组chars的长度,chars属性是一个数组,但是没有给出成员数量。chars数组到底有多少个成员,可以在为vstring分配内存时确定。struct vstring* str = malloc(sizeof(struct vstring) + n * sizeof(char)); str->len = n;上面示例中,假定chars数组的成员数量是n,只有在运行时才能知道n到底是多少。然后,就为struct vstring分配它需要的内存:它本身占用的内存长度,再加上n个数组成员占用的内存长度。最后,len属性记录一下n是多少。这样就可以让数组chars有n个成员,不用事先确定,可以跟运行时的需要保持一致。弹性数组成员有一些专门的规则。首先,弹性成员的数组,必须是 struct 结构的最后一个属性。另外,除了弹性数组成员,struct 结构必须至少还有一个其他属性。
2023年09月20日
3 阅读
0 评论
0 点赞
2023-09-20
C 语言的内存管理
C 语言的内存管理简介C 语言的内存管理,分成两部分。一部分是系统管理的,另一部分是用户手动管理的。系统管理的内存,主要是函数内部的变量(局部变量)。这部分变量在函数运行时进入内存,函数运行结束后自动从内存卸载。这些变量存放的区域称为”栈“(stack),”栈“所在的内存是系统自动管理的。用户手动管理的内存,主要是程序运行的整个过程中都存在的变量(全局变量),这些变量需要用户手动从内存释放。如果使用后忘记释放,它就一直占用内存,直到程序退出,这种情况称为”内存泄漏“(memory leak)。这些变量所在的内存称为”堆“(heap),”堆“所在的内存是用户手动管理的。void 指针前面章节已经说过了,每一块内存都有地址,通过指针变量可以获取指定地址的内存块。指针变量必须有类型,否则编译器无法知道,如何解读内存块保存的二进制数据。但是,向系统请求内存的时候,有时不确定会有什么样的数据写入内存,需要先获得内存块,稍后再确定写入的数据类型。为了满足这种需求,C 语言提供了一种不定类型的指针,叫做 void 指针。它只有内存块的地址信息,没有类型信息,等到使用该块内存的时候,再向编译器补充说明,里面的数据类型是什么。另一方面,void 指针等同于无类型指针,可以指向任意类型的数据,但是不能解读数据。void 指针与其他所有类型指针之间是互相转换关系,任一类型的指针都可以转为 void 指针,而 void 指针也可以转为任一类型的指针。int x = 10; void* p = &x; // 整数指针转为 void 指针 int* q = p; // void 指针转为整数指针上面示例演示了,整数指针和 void 指针如何互相转换。&x是一个整数指针,p是 void 指针,赋值时&x的地址会自动解释为 void 类型。同样的,p再赋值给整数指针q时,p的地址会自动解释为整数指针。注意,由于不知道 void 指针指向什么类型的值,所以不能用*运算符取出它指向的值。char a = 'X'; void* p = &a; printf("%c\n", *p); // 报错上面示例中,p是一个 void 指针,所以这时无法用*p取出指针指向的值。void 指针的重要之处在于,很多内存相关函数的返回值就是 void 指针,只给出内存块的地址信息,所以放在最前面进行介绍。malloc()malloc()函数用于分配内存,该函数向系统要求一段内存,系统就在“堆”里面分配一段连续的内存块给它。它的原型定义在头文件stdlib.h。void* malloc(size_t size)它接受一个非负整数作为参数,表示所要分配的内存字节数,返回一个 void 指针,指向分配好的内存块。这是非常合理的,因为malloc()函数不知道,将要存储在该块内存的数据是什么类型,所以只能返回一个无类型的 void 指针。可以使用malloc()为任意类型的数据分配内存,常见的做法是先使用sizeof()函数,算出某种数据类型所需的字节长度,然后再将这个长度传给malloc()。int* p = malloc(sizeof(int)); *p = 12; printf("%d\n", *p); // 12上面示例中,先为整数类型分配一段内存,然后将整数12放入这段内存里面。这个例子其实不需要使用malloc(),因为 C 语言会自动为整数(本例是12)提供内存。有时候为了增加代码的可读性,可以对malloc()返回的指针进行一次强制类型转换。int* p = (int*) malloc(sizeof(int));上面代码将malloc()返回的 void 指针,强制转换成了整数指针。由于sizeof()的参数可以是变量,所以上面的例子也可以写成下面这样。int* p = (int*) malloc(sizeof(*p));malloc()分配内存有可能分配失败,这时返回常量NULL。Null的值为0,是一个无法读写的内存地址,可以理解成一个不指向任何地方的指针。它在包括stdlib.h等多个头文件里面都有定义,所以只要可以使用malloc(),就可以使用NULL。由于存在分配失败的可能,所以最好在使用malloc()之后检查一下,是否分配成功。int* p = malloc(sizeof(int)); if (p == NULL) { // 内存分配失败 } // or if (!p) { //... }上面示例中,通过判断返回的指针p是否为NULL,确定malloc()是否分配成功。malloc()最常用的场合,就是为数组和自定义数据结构分配内存。int* p = (int*) malloc(sizeof(int) * 10); for (int i = 0; i < 10; i++) p[i] = i * 5;上面示例中,p是一个整数指针,指向一段可以放置10个整数的内存,所以可以用作数组。malloc()用来创建数组,有一个好处,就是它可以创建动态数组,即根据成员数量的不同,而创建长度不同的数组。int* p = (int*) malloc(n * sizeof(int));上面示例中,malloc()可以根据变量n的不同,动态为数组分配不同的大小。注意,malloc()不会对所分配的内存进行初始化,里面还保存着原来的值。如果没有初始化,就使用这段内存,可能从里面读到以前的值。程序员要自己负责初始化,比如,字符串初始化可以使用strcpy()函数。char* p = malloc(4); strcpy(p, "abc");上面示例中,字符指针p指向一段4个字节的内存,strcpy()将字符串“abc”拷贝放入这段内存,完成了这段内存的初始化。free()free()用于释放malloc()函数分配的内存,将这块内存还给系统以便重新使用,否则这个内存块会一直占用到程序运行结束。该函数的原型定义在头文件stdlib.h里面。void free(void* block)上面代码中,free()的参数是malloc()返回的内存地址。下面就是用法实例。int* p = (int*) malloc(sizeof(int)); *p = 12; free(p);注意,分配的内存块一旦释放,就不应该再次操作已经释放的地址,也不应该再次使用free()对该地址释放第二次。一个很常见的错误是,在函数内部分配了内存,但是函数调用结束时,没有使用free()释放内存。void gobble(double arr[], int n) { double* temp = (double*) malloc(n * sizeof(double)); // ... }上面示例中,函数gobble()内部分配了内存,但是没有写free(temp)。这会造成函数运行结束后,占用的内存块依然保留,如果多次调用gobble(),就会留下多个内存块。并且,由于指针temp已经消失了,也无法访问这些内存块,再次使用。calloc()calloc()函数的作用与malloc()相似,也是分配内存块。该函数的原型定义在头文件stdlib.h。两者的区别主要有两点:(1)calloc()接受两个参数,第一个参数是某种数据类型的值的数量,第二个是该数据类型的单位字节长度。void* calloc(size_t n, size_t size);calloc()的返回值也是一个 void 指针。分配失败时,返回 NULL。(2)calloc()会将所分配的内存全部初始化为0。malloc()不会对内存进行初始化,如果想要初始化为0,还要额外调用memset()函数。int* p = calloc(10, sizeof(int)); // 等同于 int* p = malloc(sizeof(int) * 10); memset(p, 0, sizeof(int) * 10);上面示例中,calloc()相当于malloc() + memset()。calloc()分配的内存块,也要使用free()释放。realloc()realloc()函数用于修改已经分配的内存块的大小,可以放大也可以缩小,返回一个指向新的内存块的指针。如果分配不成功,返回 NULL。该函数的原型定义在头文件stdlib.h。void* realloc(void* block, size_t size)它接受两个参数。block:已经分配好的内存块指针(由malloc()或calloc()或realloc()产生)。size:该内存块的新大小,单位为字节。realloc()可能返回一个全新的地址(数据也会自动复制过去),也可能返回跟原来一样的地址。realloc()优先在原有内存块上进行缩减,尽量不移动数据,所以通常是返回原先的地址。如果新内存块小于原来的大小,则丢弃超出的部分;如果大于原来的大小,则不对新增的部分进行初始化(程序员可以自动调用memset())。下面是一个例子,b是数组指针,realloc()动态调整它的大小。int* b; b = malloc(sizeof(int) * 10); b = realloc(b, sizeof(int) * 2000);上面示例中,指针b原来指向10个成员的整数数组,使用realloc()调整为2000个成员的数组。这就是手动分配数组内存的好处,可以在运行时随时调整数组的长度。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()不会对内存块进行初始化。restrict 说明符声明指针变量时,可以使用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()memcpy()用于将一块内存拷贝到另一块内存。该函数的原型定义在头文件string.h。void* memcpy( void* restrict dest, void* restrict source, size_t n );上面代码中,dest是目标地址,source是源地址,第三个参数n是要拷贝的字节数n。如果要拷贝10个 double 类型的数组成员,n就等于10 * sizeof(double),而不是10。该函数会将从source开始的n个字节,拷贝到dest。dest和source都是 void 指针,表示这里不限制指针类型,各种类型的内存数据都可以拷贝。两者都有 restrict 关键字,表示这两个内存块不应该有互相重叠的区域。memcpy()的返回值是第一个参数,即目标地址的指针。因为memcpy()只是将一段内存的值,复制到另一段内存,所以不需要知道内存里面的数据是什么类型。下面是复制字符串的例子。#include <stdio.h> #include <string.h> int main(void) { char s[] = "Goats!"; char t[100]; memcpy(t, s, sizeof(s)); // 拷贝7个字节,包括终止符 printf("%s\n", t); // "Goats!" return 0; }上面示例中,字符串s所在的内存,被拷贝到字符数组t所在的内存。memcpy()可以取代strcpy()进行字符串拷贝,而且是更好的方法,不仅更安全,速度也更快,它不检查字符串尾部的\0字符。char* s = "hello world"; size_t len = strlen(s) + 1; char *c = malloc(len); if (c) { // strcpy() 的写法 strcpy(c, s); // memcpy() 的写法 memcpy(c, s, len); }上面示例中,两种写法的效果完全一样,但是memcpy()的写法要好于strcpy()。使用 void 指针,也可以自定义一个复制内存的函数。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()memmove()函数用于将一段内存数据复制到另一段内存。它跟memcpy()的主要区别是,它允许目标区域与源区域有重叠。如果发生重叠,源区域的内容会被更改;如果没有重叠,它与memcpy()行为相同。该函数的原型定义在头文件string.h。void* memmove( void* dest, void* source, size_t n );上面代码中,dest是目标地址,source是源地址,n是要移动的字节数。dest和source都是 void 指针,表示可以移动任何类型的内存数据,两个内存区域可以有重叠。memmove()返回值是第一个参数,即目标地址的指针。int a[100]; // ... memmove(&a[0], &a[1], 99 * sizeof(int));上面示例中,从数组成员a[1]开始的99个成员,都向前移动一个位置。下面是另一个例子。char x[] = "Home Sweet Home"; // 输出 Sweet Home Home printf("%s\n", (char *) memmove(x, &x[5], 10));上面示例中,从字符串x的5号位置开始的10个字节,就是“Sweet Home”,memmove()将其前移到0号位置,所以x就变成了“Sweet Home Home”。memcmp()memcmp()函数用来比较两个内存区域。它的原型定义在string.h。int memcmp( const void* s1, const void* s2, size_t n );它接受三个参数,前两个参数是用来比较的指针,第三个参数指定比较的字节数。它的返回值是一个整数。两块内存区域的每个字节以字符形式解读,按照字典顺序进行比较,如果两者相同,返回0;如果s1大于s2,返回大于0的整数;如果s1小于s2,返回小于0的整数。char* s1 = "abc"; char* s2 = "acd"; int r = memcmp(s1, s2, 3); // 小于 0上面示例比较s1和s2的前三个字节,由于s1小于s2,所以r是一个小于0的整数,一般为-1。下面是另一个例子。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上面示例展示了,memcmp()可以比较内部带有字符串终止符\0的内存区域。
2023年09月20日
4 阅读
0 评论
0 点赞
2023-09-20
字符串
字符串简介C 语言没有单独的字符串类型,字符串被当作字符数组,即char类型的数组。比如,字符串“Hello”是当作数组{'H', 'e', 'l', 'l', 'o'}处理的。编译器会给数组分配一段连续内存,所有字符储存在相邻的内存单元之中。在字符串结尾,C 语言会自动添加一个全是二进制0的字节,写作\0字符,表示字符串结束。字符\0不同于字符0,前者的 ASCII 码是0(二进制形式00000000),后者的 ASCII 码是48(二进制形式00110000)。所以,字符串“Hello”实际储存的数组是{'H', 'e', 'l', 'l', 'o', '\0'}。所有字符串的最后一个字符,都是\0。这样做的好处是,C 语言不需要知道字符串的长度,就可以读取内存里面的字符串,只要发现有一个字符是\0,那么就知道字符串结束了。char localString[10];上面示例声明了一个10个成员的字符数组,可以当作字符串。由于必须留一个位置给\0,所以最多只能容纳9个字符的字符串。字符串写成数组的形式,是非常麻烦的。C 语言提供了一种简写法,双引号之中的字符,会被自动视为字符数组。{'H', 'e', 'l', 'l', 'o', '\0'} // 等价于 "Hello"上面两种字符串的写法是等价的,内部存储方式都是一样的。双引号里面的字符串,不用自己添加结尾字符\0,C 语言会自动添加。注意,双引号里面是字符串,单引号里面是字符,两者不能互换。如果把Hello放在单引号里面,编译器会报错。// 报错 'Hello'另一方面,即使双引号里面只有一个字符(比如"a"),也依然被处理成字符串(存储为2个字节),而不是字符'a'(存储为1个字节)。如果字符串内部包含双引号,则该双引号需要使用反斜杠转义。"She replied, \"It does.\""反斜杠还可以表示其他特殊字符,比如换行符(\n)、制表符(\t)等。"Hello, world!\n"如果字符串过长,可以在需要折行的地方,使用反斜杠(\)结尾,将一行拆成多行。"hello \ world"上面示例中,第一行尾部的反斜杠,将字符串拆成两行。上面这种写法有一个缺点,就是第二行必须顶格书写,如果想包含缩进,那么缩进也会被计入字符串。为了解决这个问题,C 语言允许合并多个字符串字面量,只要这些字符串之间没有间隔,或者只有空格,C 语言会将它们自动合并。char greeting[50] = "Hello, ""how are you ""today!"; // 等同于 char greeting[50] = "Hello, how are you today!";这种新写法支持多行字符串的合并。char greeting[50] = "Hello, " "how are you " "today!";printf()使用占位符%s输出字符串。printf("%s\n", "hello world")字符串变量的声明字符串变量可以声明成一个字符数组,也可以声明成一个指针,指向字符数组。// 写法一 char s[14] = "Hello, world!"; // 写法二 char* s = "Hello, world!";上面两种写法都声明了一个字符串变量s。如果采用第一种写法,由于字符数组的长度可以让编译器自动计算,所以声明时可以省略字符数组的长度。char s[] = "Hello, world!";上面示例中,编译器会将数组s的长度指定为14,正好容纳后面的字符串。字符数组的长度,可以大于字符串的实际长度。char s[50] = "hello";上面示例中,字符数组s的长度是50,但是字符串“hello”的实际长度只有6(包含结尾符号\0),所以后面空出来的44个位置,都会被初始化为\0。字符数组的长度,不能小于字符串的实际长度。char s[5] = "hello";上面示例中,字符串数组s的长度是5,小于字符串“hello”的实际长度6,这时编译器会报错。因为如果只将前5个字符写入,而省略最后的结尾符号\0,这很可能导致后面的字符串相关代码出错。字符指针和字符数组,这两种声明字符串变量的写法基本是等价的,但是有两个差异。第一个差异是,指针指向的字符串,在 C 语言内部被当作常量,不能修改字符串本身。char* s = "Hello, world!"; s[0] = 'z'; // 错误上面代码使用指针,声明了一个字符串变量,然后修改了字符串的第一个字符。这种写法是错的,会导致难以预测的后果,执行时很可能会报错。如果使用数组声明字符串变量,就没有这个问题,可以修改数组的任意成员。char s[] = "Hello, world!"; s[0] = 'z';为什么字符串声明为指针时不能修改,声明为数组时就可以修改?原因是系统会将字符串的字面量保存在内存的常量区,这个区是不允许用户修改的。声明为指针时,指针变量存储的值是一个指向常量区的内存地址,因此用户不能通过这个地址去修改常量区。但是,声明为数组时,编译器会给数组单独分配一段内存,字符串字面量会被编译器解释成字符数组,逐个字符写入这段新分配的内存之中,而这段新内存是允许修改的。为了提醒用户,字符串声明为指针后不得修改,可以在声明时使用const说明符,保证该字符串是只读的。const char* s = "Hello, world!";上面字符串声明为指针时,使用了const说明符,就保证了该字符串无法修改。一旦修改,编译器肯定会报错。第二个差异是,指针变量可以指向其它字符串。char* s = "hello"; s = "world";上面示例中,字符指针可以指向另一个字符串。但是,字符数组变量不能指向另一个字符串。char s[] = "hello"; s = "world"; // 报错上面示例中,字符数组的数组名,总是指向初始化时的字符串地址,不能修改。同样的原因,声明字符数组后,不能直接用字符串赋值。char s[10]; s = "abc"; // 错误上面示例中,不能直接把字符串赋值给字符数组变量,会报错。原因是字符数组的变量名,跟所指向的数组是绑定的,不能指向另一个地址。为什么数组变量不能赋值为另一个数组?原因是数组变量所在的地址无法改变,或者说,编译器一旦为数组变量分配地址后,这个地址就绑定这个数组变量了,这种绑定关系是不变的。C 语言也因此规定,数组变量是一个不可修改的左值,即不能用赋值运算符为它重新赋值。想要重新赋值,必须使用 C 语言原生提供的strcpy()函数,通过字符串拷贝完成赋值。这样做以后,数组变量的地址还是不变的,即strcpy()只是在原地址写入新的字符串,而不是让数组变量指向新的地址。char s[10]; strcpy(s, "abc");上面示例中,strcpy()函数把字符串abc拷贝给变量s,这个函数的详细用法会在后面介绍。strlen()strlen()函数返回字符串的字节长度,不包括末尾的空字符\0。该函数的原型如下。// string.h size_t strlen(const char* s);它的参数是字符串变量,返回的是size_t类型的无符号整数,除非是极长的字符串,一般情况下当作int类型处理即可。下面是一个用法实例。char* str = "hello"; int len = strlen(str); // 5strlen()的原型在标准库的string.h文件中定义,使用时需要加载头文件string.h。#include <stdio.h> #include <string.h> int main(void) { char* s = "Hello, world!"; printf("The string is %zd characters long.\n", strlen(s)); }注意,字符串长度(strlen())与字符串变量长度(sizeof()),是两个不同的概念。char s[50] = "hello"; printf("%d\n", strlen(s)); // 5 printf("%d\n", sizeof(s)); // 50上面示例中,字符串长度是5,字符串变量长度是50。如果不使用这个函数,可以通过判断字符串末尾的\0,自己计算字符串长度。int my_strlen(char *s) { int count = 0; while (s[count] != '\0') count++; return count; }strcpy()字符串的复制,不能使用赋值运算符,直接将一个字符串赋值给字符数组变量。char str1[10]; char str2[10]; str1 = "abc"; // 报错 str2 = str1; // 报错上面两种字符串的复制写法,都是错的。因为数组的变量名是一个固定的地址,不能修改,使其指向另一个地址。如果是字符指针,赋值运算符(=)只是将一个指针的地址复制给另一个指针,而不是复制字符串。char* s1; char* s2; s1 = "abc"; s2 = s1;上面代码可以运行,结果是两个指针变量s1和s2指向同一字符串,而不是将字符串s1的内容复制给s2。C 语言提供了strcpy()函数,用于将一个字符串的内容复制到另一个字符串,相当于字符串赋值。该函数的原型定义在string.h头文件里面。strcpy(char dest[], const char source[])strcpy()接受两个参数,第一个参数是目的字符串数组,第二个参数是源字符串数组。复制字符串之前,必须要保证第一个参数的长度不小于第二个参数,否则虽然不会报错,但会溢出第一个字符串变量的边界,发生难以预料的结果。第二个参数的const说明符,表示这个函数不会修改第二个字符串。#include <stdio.h> #include <string.h> int main(void) { char s[] = "Hello, world!"; char t[100]; strcpy(t, s); t[0] = 'z'; printf("%s\n", s); // "Hello, world!" printf("%s\n", t); // "zello, world!" }上面示例将变量s的值,拷贝一份放到变量t,变成两个不同的字符串,修改一个不会影响到另一个。另外,变量t的长度大于s,复制后多余的位置(结束标志\0后面的位置)都为随机值。strcpy()也可以用于字符数组的赋值。char str[10]; strcpy(str, "abcd");上面示例将字符数组变量,赋值为字符串“abcd”。strcpy()的返回值是一个字符串指针(即char*),指向第一个参数。char* s1 = "beast"; char s2[40] = "Be the best that you can be."; char* ps; ps = strcpy(s2 + 7, s1); puts(s2); // Be the beast puts(ps); // beast上面示例中,从s2的第7个位置开始拷贝字符串beast,前面的位置不变。这导致s2后面的内容都被截去了,因为会连beast结尾的空字符一起拷贝。strcpy()返回的是一个指针,指向拷贝开始的位置。strcpy()返回值的另一个用途,是连续为多个字符数组赋值。strcpy(str1, strcpy(str2, "abcd"));上面示例调用两次strcpy(),完成两个字符串变量的赋值。另外,strcpy()的第一个参数最好是一个已经声明的数组,而不是声明后没有进行初始化的字符指针。char* str; strcpy(str, "hello world"); // 错误上面的代码是有问题的。strcpy()将字符串分配给指针变量str,但是str并没有进行初始化,指向的是一个随机的位置,因此字符串可能被复制到任意地方。如果不用strcpy(),自己实现字符串的拷贝,可以用下面的代码。char* strcpy(char* dest, const char* source) { char* ptr = dest; while (*dest++ = *source++); return ptr; } int main(void) { char str[25]; strcpy(str, "hello world"); printf("%s\n", str); return 0; }上面代码中,关键的一行是while (*dest++ = *source++),这是一个循环,依次将source的每个字符赋值给dest,然后移向下一个位置,直到遇到\0,循环判断条件不再为真,从而跳出循环。其中,*dest++这个表达式等同于*(dest++),即先返回dest这个地址,再进行自增运算移向下一个位置,而*dest可以对当前位置赋值。strcpy()函数有安全风险,因为它并不检查目标字符串的长度,是否足够容纳源字符串的副本,可能导致写入溢出。如果不能保证不会发生溢出,建议使用strncpy()函数代替。strncpy()strncpy()跟strcpy()的用法完全一样,只是多了第3个参数,用来指定复制的最大字符数,防止溢出目标字符串变量的边界。char* strncpy( char* dest, char* src, size_t n );上面原型中,第三个参数n定义了复制的最大字符数。如果达到最大字符数以后,源字符串仍然没有复制完,就会停止复制,这时目的字符串结尾将没有终止符\0,这一点务必注意。如果源字符串的字符数小于n,则strncpy()的行为与strcpy()完全一致。strncpy(str1, str2, sizeof(str1) - 1); str1[sizeof(str1) - 1] = '\0';上面示例中,字符串str2复制给str1,但是复制长度最多为str1的长度减去1,str1剩下的最后一位用于写入字符串的结尾标志\0。这是因为strncpy()不会自己添加\0,如果复制的字符串片段不包含结尾标志,就需要手动添加。strncpy()也可以用来拷贝部分字符串。char s1[40]; char s2[12] = "hello world"; strncpy(s1, s2, 5); s1[5] = '\0'; printf("%s\n", s1); // hello上面示例中,指定只拷贝前5个字符。strcat()strcat()函数用于连接字符串。它接受两个字符串作为参数,把第二个字符串的副本添加到第一个字符串的末尾。这个函数会改变第一个字符串,但是第二个字符串不变。该函数的原型定义在string.h头文件里面。char* strcat(char* s1, const char* s2);strcat()的返回值是一个字符串指针,指向第一个参数。char s1[12] = "hello"; char s2[6] = "world"; strcat(s1, s2); puts(s1); // "helloworld"上面示例中,调用strcat()以后,可以看到字符串s1的值变了。注意,strcat()的第一个参数的长度,必须足以容纳添加第二个参数字符串。否则,拼接后的字符串会溢出第一个字符串的边界,写入相邻的内存单元,这是很危险的,建议使用下面的strncat()代替。strncat()strncat()用于连接两个字符串,用法与strcat()完全一致,只是增加了第三个参数,指定最大添加的字符数。在添加过程中,一旦达到指定的字符数,或者在源字符串中遇到空字符\0,就不再添加了。它的原型定义在string.h头文件里面。char* strncat( const char* dest, const char* src, size_t n );strncat()返回第一个参数,即目标字符串指针。为了保证连接后的字符串,不超过目标字符串的长度,strncat()通常会写成下面这样。strncat( str1, str2, sizeof(str1) - strlen(str1) - 1 );strncat()总是会在拼接结果的结尾,自动添加空字符\0,所以第三个参数的最大值,应该是str1的变量长度减去str1的字符串长度,再减去1。下面是一个用法实例。char s1[10] = "Monday"; char s2[8] = "Tuesday"; strncat(s1, s2, 3); puts(s1); // "MondayTue"上面示例中,s1的变量长度是10,字符长度是6,两者相减后再减去1,得到3,表明s1最多可以再添加三个字符,所以得到的结果是MondayTue。strcmp()如果要比较两个字符串,无法直接比较,只能一个个字符进行比较,C 语言提供了strcmp()函数。strcmp()函数用于比较两个字符串的内容。该函数的原型如下,定义在string.h头文件里面。int strcmp(const char* s1, const char* s2);按照字典顺序,如果两个字符串相同,返回值为0;如果s1小于s2,strcmp()返回值小于0;如果s1大于s2,返回值大于0。下面是一个用法示例。// s1 = Happy New Year // s2 = Happy New Year // s3 = Happy Holidays strcmp(s1, s2) // 0 strcmp(s1, s3) // 大于 0 strcmp(s3, s1) // 小于 0注意,strcmp()只用来比较字符串,不用来比较字符。因为字符就是小整数,直接用相等运算符(==)就能比较。所以,不要把字符类型(char)的值,放入strcmp()当作参数。strncmp()由于strcmp()比较的是整个字符串,C 语言又提供了strncmp()函数,只比较到指定的位置。该函数增加了第三个参数,指定了比较的字符数。它的原型定义在string.h头文件里面。int strncmp( const char* s1, const char* s2, size_t n );它的返回值与strcmp()一样。如果两个字符串相同,返回值为0;如果s1小于s2,strcmp()返回值小于0;如果s1大于s2,返回值大于0。下面是一个例子。char s1[12] = "hello world"; char s2[12] = "hello C"; if (strncmp(s1, s2, 5) == 0) { printf("They all have hello.\n"); }上面示例只比较两个字符串的前5个字符。sprintf(),snprintf()sprintf()函数跟printf()类似,但是用于将数据写入字符串,而不是输出到显示器。该函数的原型定义在stdio.h头文件里面。int sprintf(char* s, const char* format, ...);sprintf()的第一个参数是字符串指针变量,其余参数和printf()相同,即第二个参数是格式字符串,后面的参数是待写入的变量列表。char first[6] = "hello"; char last[6] = "world"; char s[40]; sprintf(s, "%s %s", first, last); printf("%s\n", s); // hello world上面示例中,sprintf()将输出内容组合成“hello world”,然后放入了变量s。sprintf()的返回值是写入变量的字符数量(不计入尾部的空字符\0)。如果遇到错误,返回负值。sprintf()有严重的安全风险,如果写入的字符串过长,超过了目标字符串的长度,sprintf()依然会将其写入,导致发生溢出。为了控制写入的字符串的长度,C 语言又提供了另一个函数snprintf()。snprintf()只比sprintf()多了一个参数n,用来控制写入变量的字符串不超过n - 1个字符,剩下一个位置写入空字符\0。下面是它的原型。int snprintf(char*s, size_t n, const char* format, ...);snprintf()总是会自动写入字符串结尾的空字符。如果你尝试写入的字符数超过指定的最大字符数,snprintf()会写入 n - 1 个字符,留出最后一个位置写入空字符。下面是一个例子。snprintf(s, 12, "%s %s", "hello", "world");上面的例子中,snprintf()的第二个参数是12,表示写入字符串的最大长度不超过12(包括尾部的空字符)。snprintf()的返回值是写入格式字符串的字符数量(不计入尾部的空字符\0)。如果n足够大,返回值应该小于n,但是有时候格式字符串的长度可能大于n,那么这时返回值会大于n,但实际上真正写入变量的还是n-1个字符。如果遇到错误,返回一个负值。因此,返回值只有在非负并且小于n时,才能确认完整的格式字符串写入了变量。字符串数组如果一个数组的每个成员都是一个字符串,需要通过二维的字符数组实现。每个字符串本身是一个字符数组,多个字符串再组成一个数组。char weekdays[7][10] = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" };上面示例就是一个字符串数组,一共包含7个字符串,所以第一维的长度是7。其中,最长的字符串的长度是10(含结尾的终止符\0),所以第二维的长度统一设为10。因为第一维的长度,编译器可以自动计算,所以可以省略。char weekdays[][10] = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" };上面示例中,二维数组第一维的长度,可以由编译器根据后面的赋值,自动计算,所以可以不写。数组的第二维,长度统一定为10,有点浪费空间,因为大多数成员的长度都小于10。解决方法就是把数组的第二维,从字符数组改成字符指针。char* weekdays[] = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" };上面的字符串数组,其实是一个一维数组,成员就是7个字符指针,每个指针指向一个字符串(字符数组)。遍历字符串数组的写法如下。for (int i = 0; i < 7; i++) { printf("%s\n", weekdays[i]); }
2023年09月20日
1 阅读
0 评论
0 点赞
1
...
10
11
12
...
14