前言
我的第一门语言就是C,但是学艺不精,中途跑去学了C#和Java后,感觉到了C的重要性,毕竟是最接近底层的语言,又跑回来学C。
毕竟前两门的控制语句,变量什么的都是类似的,回到C后只需要学习一些特定C的语法,比如宏,预编译指令等等,这些对我来说都是陌生的词汇。
所以边学边记录一下以前的知识。
文章目录
一、字符串
在 C 语言中,字符串实际上是使用空字符 \0 结尾的一维字符数组。因此,\0 是用于标记字符串的结束。
空字符(Null character)又称结束符,缩写 NUL,是一个数值为 0 的控制字符,\0 是转义字符,意思是告诉编译器,这不是字符 0,而是空字符。
char site[7] = {‘R’, ‘U’, ‘N’, ‘O’, ‘O’, ‘B’, ‘\0’};
二、结构体
C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book;
结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。
//此结构体的声明包含了其他的结构体
struct COMPLEX
{
char string[100];
struct SIMPLE a;
};
//此结构体的声明包含了指向自己类型的指针
struct NODE
{
char string[100];
struct NODE *next_node;
};
如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明
struct B; //对结构体B进行不完整声明
//结构体A中包含指向结构体B的指针
struct A
{
struct B *partner;
//other members;
};
//结构体B中包含指向结构体A的指针,在A声明完后,B也随之进行声明
struct B
{
struct A *partner;
//other members;
};
1)结构体初始化
#include <stdio.h>
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
} book = {
"C 语言", "RUNOOB", "编程语言", 123456};
int main()
{
printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book.title, book.author, book.subject, book.book_id);
}
2)结构体的访问
#include <stdio.h>
#include <string.h>
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
};
int main( )
{
struct Books Book1; /* 声明 Book1,类型为 Books */
struct Books Book2; /* 声明 Book2,类型为 Books */
/* Book1 详述 */
strcpy( Book1.title, "C Programming");
strcpy( Book1.author, "Nuha Ali");
strcpy( Book1.subject, "C Programming Tutorial");
Book1.book_id = 6495407;
/* Book2 详述 */
strcpy( Book2.title, "Telecom Billing");
strcpy( Book2.author, "Zara Ali");
strcpy( Book2.subject, "Telecom Billing Tutorial");
Book2.book_id = 6495700;
/* 输出 Book1 信息 */
printf( "Book 1 title : %s\n", Book1.title);
printf( "Book 1 author : %s\n", Book1.author);
printf( "Book 1 subject : %s\n", Book1.subject);
printf( "Book 1 book_id : %d\n", Book1.book_id);
/* 输出 Book2 信息 */
printf( "Book 2 title : %s\n", Book2.title);
printf( "Book 2 author : %s\n", Book2.author);
printf( "Book 2 subject : %s\n", Book2.subject);
printf( "Book 2 book_id : %d\n", Book2.book_id);
return 0;
}
3)作为形参
#include <stdio.h>
#include <string.h>
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
};
/* 函数声明 */
void printBook( struct Books book );
int main( )
{
struct Books Book1; /* 声明 Book1,类型为 Books */
struct Books Book2; /* 声明 Book2,类型为 Books */
/* Book1 详述 */
strcpy( Book1.title, "C Programming");
strcpy( Book1.author, "Nuha Ali");
strcpy( Book1.subject, "C Programming Tutorial");
Book1.book_id = 6495407;
/* Book2 详述 */
strcpy( Book2.title, "Telecom Billing");
strcpy( Book2.author, "Zara Ali");
strcpy( Book2.subject, "Telecom Billing Tutorial");
Book2.book_id = 6495700;
/* 输出 Book1 信息 */
printBook( Book1 );
/* 输出 Book2 信息 */
printBook( Book2 );
return 0;
}
void printBook( struct Books book )
{
printf( "Book title : %s\n", book.title);
printf( "Book author : %s\n", book.author);
printf( "Book subject : %s\n", book.subject);
printf( "Book book_id : %d\n", book.book_id);
}
4)指向结构体的指针
您可以定义指向结构的指针,方式与定义指向其他类型变量的指针相似,如下所示:
struct Books *struct_pointer;
让指向该结构体的指针,去访问结构内部的成员,您必须使用 -> 运算符,如下所示:
struct_pointer->title;
#include <stdio.h>
#include <string.h>
struct Books
{
char title[50];
char author[50];
char subject[100];
int book_id;
};
/* 函数声明 */
void printBook( struct Books *book );
int main( )
{
struct Books Book1; /* 声明 Book1,类型为 Books */
struct Books Book2; /* 声明 Book2,类型为 Books */
/* Book1 详述 */
strcpy( Book1.title, "C Programming");
strcpy( Book1.author, "Nuha Ali");
strcpy( Book1.subject, "C Programming Tutorial");
Book1.book_id = 6495407;
/* Book2 详述 */
strcpy( Book2.title, "Telecom Billing");
strcpy( Book2.author, "Zara Ali");
strcpy( Book2.subject, "Telecom Billing Tutorial");
Book2.book_id = 6495700;
/* 通过传 Book1 的地址来输出 Book1 信息 */
printBook( &Book1 );
/* 通过传 Book2 的地址来输出 Book2 信息 */
printBook( &Book2 );
return 0;
}
void printBook( struct Books *book )
{
printf( "Book title : %s\n", book->title);
printf( "Book author : %s\n", book->author);
printf( "Book subject : %s\n", book->subject);
printf( "Book book_id : %d\n", book->book_id);
}
5)结构体占用空间内存大小
01.struct tagApple
02.{
03. char A;
04. int B;
05. short C;
06.}apple;
我们都知道,char类型占用1个字节,int型占用4个字节,short类型占用2个字节,long占用4个,double占用8个;
那么我们可能会犯一个错误就是直接1+4+2=7,该结构体占用7个字节。这是错的。
以下我们简单分析下:
计算结构体大小时需要考虑其内存布局,结构体在内存中存放是按单元存放的,每个单元多大取决于结构体中最大基本类型的大小。
对格式一:
以int型占用4个来作为倍数,因为A占用一个字节后,B放不下,所以开辟新的单元,然后开辟新的单元放C,所以格式一占用的字节数为:3*4=12;
三、共用体
共用体的格式是和结构体是类似的。是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。
您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值
。共用体提供了一种使用相同的内存位置的有效方式。
union Data
{
int i;
float f;
char str[20];
} data;
1)共用体的内存大小
下面的实例将显示上面的共用体占用的总内存大小:
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main( )
{
union Data data;
printf( "Memory size occupied by data : %d\n", sizeof(data));
return 0;
}
打印出来结果是20
2)访问成员变量
我们在前面说过可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值
。
下面这个例子就来看看访问成员变量以及值的问题。
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main( )
{
union Data data;
data.i = 10;
data.f = 220.5;
strcpy( data.str, "C Programming");
printf( "data.i : %d\n", data.i);
printf( "data.f : %f\n", data.f);
printf( "data.str : %s\n", data.str);
return 0;
}
运行后结果如下:
在这里,我们可以看到共用体的 i 和 f 成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。
那我们注释掉strcpy( data.str, “C Programming”);会怎么样呢??
此时此刻结果就是f的值出来了
那我们继续注释掉f
结果不言而喻,肯定是i输出正常,于是就证明了同个时间只能有一个成员带有值,毕竟这三个变量是占一个地址的,也是有原因的。
3)如何同时访问共用体成员
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main( )
{
union Data data;
data.i = 10;
printf( "data.i : %d\n", data.i);
data.f = 220.5;
printf( "data.f : %f\n", data.f);
strcpy( data.str, "C Programming");
printf( "data.str : %s\n", data.str);
return 0;
}
在这里,所有的成员都能完好输出,因为同一时间只用到一个成员。
四、结构体的定制版——位域
位域位域,位就是操作二进制的。
1)采用传统结构体
struct
{
unsigned int A;
unsigned int B;
} status;
在结构体中,我们知道怎么计算一个结构体占用空间内存大小。此时两个int成员变量,每个各占了4个字节,2*4=8。
Bit | Bit | Bit | Bit | Bit | Bit | Bit | Bit |
---|---|---|---|---|---|---|---|
A | A | A | A | A | A | A | A |
B | B | B | B | B | B | B | B |
这样虽然占满了空间,但是我们不用到这么多bit啊,蹲坑不拉屎相当于浪费内存,因为我们只需要一个Bit。
2)采用位域
位域是 C 语言中一种使用表示成员变量的二进制位的方式,可以有效地节省内存空间。
在定义结构体时,使用冒号 : 来定义某个二进制位的字段,如下所示:
struct MyStructure {
unsigned int age : 5; // 占用 5 个二进制位
unsigned int gender : 1; // 占用一个二进制位
// 其他成员变量
};
在上面的例子中,age
和 gender
是结构体 MyStructure
的两个成员变量。冒号后面的数字表示这个成员变量占据的二进制位数(Bit),8个二进制位组成一个字节。
例如,age
变量占用了 5 个二进制位,它可表示的数值范围为 0 ~ 31。
那么此时上面的结构体占用了多少内存空间呢?
此时此刻他们占用了这个结构体占用了 1 个字节(8 比特)的内存空间。
如图所示,下面是一个字节:
Bit | Bit | Bit | Bit | Bit | Bit | Bit | Bit |
---|---|---|---|---|---|---|---|
age | age | age | age | age | gender |
其中,age
和 gender
都是使用了位域(bit-field)来定义的变量。
age
使用了 5 个二进制位来表示,因此它的取值范围是 0-31。gender
只需要一个 Bit 来表示,因此它只有两种取值方式:0 或者 1。
由于 C 语言中最小的可寻址内存单位是一个字节(即 8 Bit),这里占用了整整一个字节的内存空间。
在这个结构体中,虽然只有两个位域变量,但是实际上由于内存对齐等原因可能会产生填充,于是会填充到age
后面去,不会再重新创建一个字节去存放gender
成员变量。
再举个栗子:请问下面代码占用了多大的内存空间?
struct
{
unsigned int A: 1;
unsigned int B: 1;
} status;
这个结构体还是只占用了 1 个字节(8 Bit)的内存空间。
其中,A 和 B 都是使用了位域(Bit-field)来定义的变量,它们都只需要占用 1 Bit 的存储空间,因为它们只有两种状态(0 或 1),所以每个变量都可以用一个 Bit 表示。
由于 C 语言中最小的可寻址内存单位是一个字节(即 8 Bit),因此就算这个结构体中只有两个位域变量,也需要占用至少一个字节的内存空间。在这个结构体中,status 只包含了两个位域成员,因此它所有成员共占用了 2 Bit 的空间(1 Bit * 2),但是实际上由于内存对齐等原因可能会产生填充。
如图所示,下面是一个字节:
Bit | Bit | Bit | Bit | Bit | Bit | Bit | Bit |
---|---|---|---|---|---|---|---|
A | B |
看出来使用位域有什么好处了吧,是能大大节省我们的内存空间,这对于性能的提升等等都有不小的好处。
3)位域溢出
当位域只有3个Bit时,我们用4个BIt会出现什么情况呢?
#include <stdio.h>
#include <string.h>
struct
{
unsigned int age : 3;
} Age;
int main( )
{
Age.age = 4;
printf( "Sizeof( Age ) : %d\n", sizeof(Age) );
printf( "Age.age : %d\n", Age.age );
Age.age = 7;
printf( "Age.age : %d\n", Age.age );
Age.age = 8; // 二进制表示为 1000 有四位,超出
printf( "Age.age : %d\n", Age.age );
return 0;
}
Sizeof( Age ) : 4
Age.age : 4
Age.age : 7
Age.age : 0
4)位域的注意事项
而使用位域时首先需要注意以下几点:
- 不同编译器对于位域的实现可能有所区别,因此需要特别小心。
- 在同一个字节中不能定义重叠的位域。
- 如果结构体中包含多个位域,则它们必须按照从高到低或从低到高的顺序排列。
- 对于有符号数值类型,在使用负数时需要格外小心。
使用位域时还需要考虑:
-
具体的编译器实现和硬件架构等因素对内存布局的影响。
-
不同编译器对于位域大小和对齐方式的处理可能会有所不同,如果不同的编译器对位域大小和对齐方式处理不一致,可能会导致代码可移植性问题。
-
在使用位域时应该避免同时定义多个不同类型和大小的位域,否则可能会导致对齐和填充问题。
总之,位域是一种很方便但也很复杂的内存管理方式,在使用时需要特别小心。