本博文是在对C语言有一定深入了解后,对C语言最为主要的32个关键字进行了简要的概述和一些容易被忽略的细节研究,您可以当作学习或复习C语言基础使用(毕竟关键字就是构成C语言语法的基石),也可以提出您所不认同的点。当然,如果您有兴趣的话,可以看看我之前写的C语言入门和C语言深入系列。最后,写文难免会有所遗漏,还望君多上机躬行,莫要过信。不过,我相信您此行是必定有所收获的!
C语言关键字起码有32个(C90),后面又新增加了5个关键字(C99)。
C语言生成可执行程序的简单理解:
文本代码 -> 可执行程序(简单理解为二进制文件) -> 在Windows中可以使用鼠标双击.exe启动程序。
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
int main()
{
printf("hello word!\n");
system("pause");
return 0;
}//x64的Debug环境
如果在vs2022 Debug模式下运行了上述代码,就会在x64文件中出现Debug文件,其中会有一个.exe文件。如果不需要这些转换后的二进制文件,可以点击“生成->清理解决方案”。
另外:
数据类型 变量名 = 初始化值;//定义+初始化
数据类型 变量名;//定义
变量名字 = 赋值值;//赋值
//注意这两种写法细说还是有所区别的,只是结果等价
一台计算机在计算之前,需要数据,但是不是所有数据都要被立马计算的。因此有效数据就需要先保存起来,等待后续处理,而且效率高,这就是变量的意义。
生命周期描述一个变量的存在时间(“什么时候开辟----什么时候释放”之间的时间)。作用域描述一个变量可以被使用的有效区域。
实际上存数据的时候才不管是什么变量类型,先把要存储数据按照整形数据和浮点数据存储,最后再根据数据类型来解读数据,这就跟以前“指针变量是根据指针类型来读取数据”这一知识点串联起来了
//注意在补码转化为原码的时候,推荐使用方法二,因为更加符合计算机的工作转化,而非使用方法一
//方法一
1111 1111 1111 1111 1111 1111 1110 1100 //补码1111 1111 1111 1111 1111 1111 1110 1011 //反码
1000 0000 0000 0000 0000 0000 0001 0100 //补码(注意符号位有可能参与运算)
//方法二(更加符合计算流程)
1111 1111 1111 1111 1111 1111 1110 1100 //补码
1000 0000 0000 0000 0000 0000 0001 0011 //反码
1000 0000 0000 0000 0000 0000 0001 0100 //补码(注意符号位有可能参与运算)
#include
int main()
{
unsigned char ch = -259;
//-259 = 1000 0000|0000 0000|0000 0001|0000 0011
// 1111 1111|1111 1111|1111 1110|1111 1100
// 1111 1111|1111 1111|1111 1110|1111 1101
//ch是unsigned char类型,开辟一个字节空间,存入截断后的数据“1111 1101”
printf("%u\n", ch);//将数据作为无符号字符型理解:“1111 1101”变成“0000 0000|0000 0000|0000 0000|1111 1101”,转为原码,得到253
printf("%d\n", ch);//将数据作为有符号字符型理解:“1111 1101”变成“0000 0000|0000 0000|0000 0000|1111 1101”,转为原码,得到253
unsigned int in = -2;
//-2 = 1000 0000|0000 0000|0000 0000|0000 0010
// 1111 1111|1111 1111|1111 1111|1111 1101
// 1111 1111|1111 1111|1111 1111|1111 1110
//in是unsigned int类型,开辟四个字节空间,存入数据“1111 1111|1111 1111|1111 1111|1111 1110”
printf("%u\n", in);//将数据作为无符号整型理解:“1111 1111|1111 1111|1111 1111|1111 1110”,转为原码,得到4294967294
printf("%d\n", in);//将数据作为有符号整型理解:“1111 1111|1111 1111|1111 1111|1111 1110”,转为原码,得到-2
return 0;
}
//数据先按照自己的值类型转为二进制,再按照所给类型存储,再按照所给转化说明读取
这个比较复杂,不在本次多说
//在signed char的情况下,补码1000 0000会被识别为-128,也就是说一个signed char类型就竟然能存储一个9比特位数字!!!
signed char ch = -128;
//存数据
//-128 = 1 1000 0000(原码)
// 1 0111 1111(反码)
// 1 1000 0000(补码)
//ch开辟了一个字节的空间进行存储,存储了“1000 0000”,注意这里发生了截断,计算机识别1000 0000为-128的补码
printf("%d", ch);
//取数据
//规定看到1000 0000时,不用按照原反补变化取回数据,而是直接规定为-128
//正常被打印出-128
//题目练习一
char a[1000];
for ( int i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));//请问这里会打印多少呢?输出255
//题目练习二
#include
int main()
{
int i = -20;
// i = -20
// = 1000 0000|0000 0000|0000 0000|0001 0100(原码)
// = 1111 1111|1111 1111|1111 1111|1110 1011(反码)
// = 1111 1111|1111 1111|1111 1111|1110 1100(补码)
unsigned int j = 10;
// j = 10
// = 0000 0000|0000 0000|0000 0000|0000 1010(原码/反码/补码)
//数据计算的本质,是内存中的二进制序列进行计算
printf("%d\n", i + j);//i + j本身整体是unsigned int类型(隐式类型转化的缘故,可以用编译器验证),只是解读的方式不同
//1111 1111|1111 1111|1111 1111|1110 1100(补码)
//0000 0000|0000 0000|0000 0000|0000 1010(原码/反码/补码)
// +
//----------------------------------------
//1111 1111|1111 1111|1111 1111|1111 0110
//根据%d,则解释为signed int,由上面的二进制序列,得出结果为-10
printf("%u\n", i + j);//i + j本身整体是unsigned int类型(隐式类型转化的缘故,可以用编译器验证),只是解读的方式不同
//根据%u,则解释为unsigned int,由上面的二进制序列,得出结果为4294967286
return 0;
}
//题目练习三
#include
#include
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)//无效的循环写法,会陷入死循环
{
printf("%u\n", i);
Sleep(1000);
}
return 0;
}
常见的数据存储硬件
寄存器
缓存:L1cache/L2cache/L3cache
内存:DRAM芯片
硬盘:HDD/SSD/flash
光盘
软盘
磁带
离CPU越近的越贵,速度越快
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test1.c文件内部
#include
extern int c;//声明外部变量
extern void test(void); //声明外部函数
int main()
{
printf("%d\n", c);//使用外部变量
test();//使用外部函数
return 0;
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test2.c文件内部
#include
int c = 100;
void test(void)
{
printf("limou\n");
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test.h头文件内部
#define <stdio.h>
extern int val;
extern void test(void);
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在main.c源文件内部
#include "test.h"//主要目的是为了使用“变量/函数”的声明
int main()
{
printf("%d\n", val);
test();
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test.c源文件内部
#include "test.h"//主要目的是为了使用stdio头文件
int val = 100;
void test(void)
{ printf("limou\n");
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
使得变量变成静态全局变量,其作用域仅限于该变量被定义的地方开始,因此该变量不能被直接跨文件使用(但是可以间接使用)
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test1.c文件内部
#include
extern int c;//声明外部变量
extern void test(void); //声明外部函数
int main()
{
printf("%d\n", c);//使用外部变量,但是无法直接使用了
test();//使用外部函数,可以看到test函数调用了其定义所在文件的静态变量,这个就是间接使用
return 0;
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test2.c文件内部
#include
static int c = 100;
void test(void)
{
printf("limou:%d\n", c);//静态全局变量只能在本文件被直接使用
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
使得函数变成静态函数,其作用域仅限于该函数被定义的地方开始,因此该函数不能被直接跨文件使用(但是可以间接使用)
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test1.c
#include
extern void test(void);//声明外部函数
extern void (*ptest)(void);//声明外部函数指针变量
extern void fun(void);//声明外部函数
int main()
{
test();//直接使用外部函数,没有办法直接被使用
(*ptest)();//通过函数指针间接使用,调用函数成功
fun();
return 0;
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test.2
#include
int c = 100;
static void test(void)//被static修饰
{
printf("limou:%d\n", c);
}
//间接使用方法1
void (*ptest)(void) = &test;
//间接使用方法2
void fun(void)
{
test();
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
和上面的全局变量和函数的使用意义有些不同,static修饰局部变量,则局部变量会被转存到内存的静态区,局部变量正常来说本该被销毁释放,但是通过下面的指针我们可以发现其并没有被销毁,因此其生命周期变成全局变量生命周期(并不是直接变成全局变量,只是在生命周期上是具有全局变量的特征而已,被static修饰的局部变量作用于没有被改变)
int* p = NULL;
void fun()
{
static int i = 0;
i++;
p = &i;
printf("%d ", i);
}
int main()
{
fun();
printf("%p", p);
return 0;
}
那么局部变量被static修饰后改变了生命周期的原因是什么呢?本质就是static修饰的局部变量从“栈区(临时性)”转移到了“全局数据区”,但是注意这是操作系统的概念,并不是C语言中的概念
在C++中,static还有一个作用,但是这就另说了
这个关键字比较好理解,就是声明一个变量/函数,尽管函数的声明不需要依靠extern,但是就代码整洁规范来说,最好还是要加上extern来显示声明
实际上sizeof是一个关键字/操作符,而不是一个函数,其作用是计算变量类型大小,使用的单位是字节,在编译期间就起效果了
#define _CRT_SECURE_NO_WARNINGS 1
#include
int main()
{
int a = 10;
printf("%zd ", sizeof(a));
printf("%zd ", sizeof(int));
printf("%zd ", sizeof a);//从这里也可以看出sizeof不是函数,“()”可以理解为函数调用时的操作符
printf("%zd ", sizeof int);
return 0;
}
在sizeof的使用中,类型必须加括号,因此上面四条语句只有前三句是正确的
可以使用if注释,当然有点好笑,但是没问题不是么,但是不推荐这种方式,但是在以前是真的有程序员写出类似的“代码注释”的,只需要知道有这种方式就可以了
if(0)//因为没有机会被编译运行
{
某些代码或注释
}
bool变量在很多高级语言都有,有两个取值true和false,一般来说C语言在C89、C90中是没有bool类型的,但是C99引入了_Bool类型,在新增的头文件stdbool.h中,又被重新用宏写成了bool,这是为了保证C和C++之间的兼容性(注意和C不同,C++中本就有bool值的)
#include
#include //bool的头文件
#include //BOOL的头文件
int main()
{
//这个是库中的bool,可移植性高(推荐使用)
bool x = false;
printf("%zd\n", sizeof(x));
//这个BOOL是微软进行重命名的,可移植性低(不推荐使用)
BOOL y = TRUE;
printf("%zd\n", sizeof(y));
return 0;
}
上述代码说明bool类型在较新标准的C语言中是有大小的,大小为1个字节。
但是微软的是将int类型重命名为BOOL,因此大小就是int的大小为4个字节。
#include
#include //bool的头文件
#include //BOOL的头文件
int main()
{
int flag = 0;
if (flag == 0)
printf("flag == 0\n");
if (flag == false)
printf("flag == false\n");
if (!flag)
printf("!flag\n");
return 0;
}
第一种容易误会,误以为是整数的比较,而不是判断真假
第二种如果没有包含头文件stdbool.h就识别不出来
实际上第三种是最推荐的,对于C程序员来说,更加的符合直觉,一眼就能看出这个条件语句在判断真假,因此在包含头文件的情况下也推荐这么写
int main()
{
bool a = false;
//某些code
if(!a)//利用布尔值的话更加直观
{
printf("haha\n");
}
return 0;
}
计算if判断语句中的真假
值判定真假值
进入分支语句
if ( ((x - y) > -精度) && ((x - y) < 精度) )
if ( (fabs(x - y) < 精度) )//注意需要使用math头文件才可以使用函数fabs
#include //可以使用这两个精度 DBL_EPSILON 2.2204460492503131e-016 FLT_EPSILON 1.192092896e-07F //DBL_EPSILON是满足“DBL_EPSILON+n != n”的最小正数,但是这个DBL_EPSILON+n依旧会引发n的大小变动 //这个精度最好直接看头文件的定义处
//假设要判断一个数是否等于0,若使用以下语句
(fabs(x) <= DBL_EPSILON)
//“等于”则说明x就是能够引起其他数变化的值,这就和0的概念相矛盾(任何数+0都不变)
//写法一:最为标准的写法,清晰明了
int a = 10;
int* p = &a;
if( NULL == p )//判空的较好写法
{
//…某些语句
}
//写法二:( p == 0 ),容易误解pa为整型
//写法三:( !p ),容易误解p为bool类型
if和else是就近匹配使用的
int i = 0, j = 0;
scanf("%d %d", &i, &j);
if( i == 0 )
if( i == 0 )
printf("%d", i);
else
printf("%d", i);
//C语言对缩进并不敏感,但是好的缩进能增加对代码的理解,所以上面code正确的缩进应该是
int i = 0, j = 0;
scanf("%d %d", &i, &j);
if( i == 0 )
if( i == 0 )
printf("%d", i);
else
printf("%d", i);
//而最好的办法就是加花括号
int i = 0, j = 0;
scanf("%d %d", &i, &j);
if( i == 0 )
{
if( i == 0 )
{
printf("%d", i);
}
else
{
printf("%d", i);
}
}
在分支过多的时候可以使用switch,缺点是对于范围判断比较难处理,对于default选项建议还是加上
只能是整型常量或整型表达式,并且在C语言里也不能放const修饰的变量
一般来说常用的选项放在前面,不常用的放在后面
在一个case语句的内部中,不支持在多条语句中直接放入定义语句,当时若是写成代码块的形式就支持
#include
//写法一
int main()
{
int i = 0;
scanf("%d", &i);
switch (i)
{
case 1:
int j = 0;//不支持,报错
printf("a");
printf("b");
break;
case 2:
printf("c");
}
return 0;
}
//写法二
int main()
{
int i = 0;
scanf("%d", &i);
switch (i)
{
case 1:
{
int j = 0;//支持,不报错
printf("a");
printf("b");
break;
}
case 2:
printf("c");
}
return 0;
}
如果代码很多,使用return可能会被误认为是break,不好维护代码
不然会出现能到达某个case语句的情况
不应该把某些选项的情况交给default来处理(即偷懒把某个case改造成default)…
switch作为分支语法提示
case完成判定功能
break完成分支功能
default完成异常情况处理
有关C语言内三种循环的详细使用在这里就不多讲了
就是直接跳出循环体
实际上在现实生活中,大型项目也有大量使用go to语句,并不是想象中的没人使用
#include
int main()
{
printf("%zd\n", sizeof(void));
return 0;
}
(void)i;//不合法
作为空类型,理论上是不应该开辟空间的,即使开辟了空间,也仅仅作为一个占位符来看待,因此不能使用void来创建void类型的变量
不要返回指向“栈内存”的“指针”,因为该内存在函数体结束的时候会被自动销毁,访问这个指针指向的内容这将带来风险
int fun()
{
int number = 10;
return &number;
}
int main()
{
int* i = fun();
printf("%d", *i);//打印出乱码
return 0;
}
明明局部变量被销毁了,为什么还能返回值呢?
int fun(void)
{
int i = 10;
return i;//return把“变量/表达式”的值被放进了CPU的寄存器里
}
int mian()
{
int y = fun();//将寄存器里的值放入y中,如果不拿y接收就不对寄存器里的值做处理
return 0;
}
因此,“被调用函数”的返回值是通过寄存器的方式返回“函数调用方”的
是一个空的 return 语句,常用于函数的最后,用于结束函数的执行并返回调用者。这里的分号表示语句结束的标志,表明函数的返回值为空
int main()
{
char* p = "hello word!";
*p = 'H';//这是不允许被直接修改的,也不允许间接修改
return 0;
}
实际上放在类型前和类型后是没有区别的
int const i = 10;//可以这么写但是不推荐
注意被const修饰后的类型和原类型在编译器看来可能不是一个类型
const的意义
const int a;
a = 10;//这是不被允许的
有的环境编译不过(VS2022),有的环境编译得过(Linux下的gcc编译器),但是如果向标C看齐的话,就是不可以
修饰数组时就是把数值变成只读数值,即:每个元素都是只读得
int i = 10;
const int *p = &i;//p存放“指向i的地址”,p指向的是int类型的const值
int const *p = &i;//p存放“指向i的地址”,p指向的是int类型的const值
int* const p = &i;//p存放“指向i的地址”,p是被const修饰的常变量
const int *const p = &i;p存放“指向i的地址”,p是const修饰的常变量,p指向的是int类型的const值
int i = 10;
int* p = &i;
const int * const * const pp = &p;
//二级指针
int x = 10;
int* px = &x;
const int * * ppx = &px;
**ppx = 100;//非法
*ppx = 100;
ppx = 100;
int y = 10;
int* py = &y;
int * const * ppy = &py;
**ppy = 100;
*ppy = 100;//非法
ppy = 100;
int z = 10;
int* pz = &z;
int* * const ppz = &pz;
**ppz = 100;
*ppz = 100;
ppz = 100;//非法
int k = 10;
int* pk = &k;
const int * const * const ppk = &pk;
**ppk = 100;//非法
*ppk = 100;//非法
ppk = 100;//非法
//三级指针
int k = 10;
int* pk = &k;
int** ppk = &pk;
//const int * * * pppk = &ppk;//const修饰第一个*,代表**pppk指向一个const值(指向的类型是int),即不能通过**pppk来改变其指向的内容
//int * const * * pppk = &ppk;//const修饰第二个*,代表*pppk指向一个const值(指向的类型是int*),即不能通过*pppk来改变其指向的内容
//int * * const * pppk = &ppk;//const修饰第三个*,代表pppk指向一个const值(指向的类型是int**),即不能通过pppk来改变其指向的内容
//int * * * const pppk = &ppk;//const修饰pppk,代表pppk本身不能被直接修改(本身的类型是int***),即不能直接修改pppk本身的内容
//***pppk = 100;
//**pppk = 100;
//*pppk = 100;
//pppk = 100;
实际上这是一种预防性的编程,也是const用的最多的地方
const int* GetVal()//这样返回的指针不会在调用函数内被直接修改(尤其是那些需要修改字符串然后返回字符地址的代码)
{
static int a = 10;
return &a;
}
int main()
{
const int* p = GetVal();
//*p = 100;//不合法
return 0;
}
实际上内置类型返回,加上const是没有什么意义的(本例子是指针类型,并不是内置类型)
直白翻译就是易变的、不稳定的意思,被这个关键字修饰的变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其他线程等。遇到这个关键声明的变量,编译器对访问该变量的代码不再进行优化,从而提供特殊地址的“稳定访问”(即“不用volatile修饰的代码有可能会被编译器优化”)。
另外在Java中也有个类似的并且还多一个“指令重排”的功能.
在应用开发、单进程的程序中是基本不会用到volatile字的。
volatile int i = 1;//这里的代码会导致代码不会优化(代码会被编译器优化修改,变得不再从内存读取数据i的内容(因为是死循环代码)),而保持内存的可见性(被CPU看到)
int main()
{
while(i);//这个语句编译器有可能进行优化,这在反汇编的时候可以查看一下代码变化
return 0;
}
以上代码可以在Linux中测试其反汇编,来查看volatile的影响。
由于翻译的原因,如果这两个关键字共用,很有可能会有人误会这里两个关键字没办法共用(无法修改易变的)。
但是实际上const是要求不进行写入(考虑写的问题),volatile是要求每次读取数据的时候都要从内存读取(考虑读的问题),因此两者并不冲突。
在现实场景中仅靠int、float是不够的,因此产生了结构体的概念,C努力将结构体和变量的行为从应用的角度上一样(例如不像传数组只能传地址,结构体可以传值)
struct 结构名字
{
结构体成员1;
结构体成员2;
结构体成员3;
…;
};//注意不要忘记这个分号!!!
值得注意的是,只能在定义的时候进行初始化,不能在定义后进行赋值(如果一定要这么做,只能通过结构体成员访问符“.”和“->”来做到)
struct Datas
{
int data1;
char data2;
double data[10];
float data3;
};
int main()
{
struct Datas a;
a = { 1, 2, { 1, 2, 3, 4 }, 5 };//这个语句是错误的
return 0;
}
struct Datas
{
int data1;
char data2;
double data[10];
float data3;
};
int main()
{
struct Datas a = { 1, 2, { 1, 2, 3, 4 }, 5 };//正确写法
return 0;
}
还有一个问题就是不能将字符串直接赋值给字符数组,这是因为字符数组也类似结构体,具有“只能在定义的时候初始化”这一特性
struct Datas
{
int data;
char str[10];
};
int main()
{
struct Datas a;
a.data = 1;
a.str = "abcd";//不合法,只能采用字符串拷贝函数进行赋值
return 0;
}
/* 类似这样书写代码,也是不合法的
int arr[10];
arr = { 1, 2, 3, 4, 5, 6, 7, 8 };
*/
struct Datas
{
int data;
char str[10];
};
int main()
{
struct Datas a = { 1, "abcd" };//要么是直接初始化,要么是使用strcpy库函数
return 0;
}
#include
//如果关于strcpy函数的使用出现警告,则可以采用语句:#pragma warning(disable:4996)忽略掉错误
{
int data;
char str[10];
};
int main()
{
struct Datas a;
a.data = 1;
strcpy(a.str, "abcd");
return 0;
}
注意结构体指针在数值等于其成员的最小地址
和环境有关,没有准确的答案。甚至在有的编译器里直接就不允许定义空结构体(比如VS2022编译器),而有的编译器会认为空结构体的大小是0,如果拿它定义变量就会得到大小为0的变量,注意这也是由编译器决定的,莫要记死!
C99标准新增加的功能(跨平台性可能不太好),柔性数组实际上是为了方便动态开辟内存空间而设计的,其只能在结构体内部定义,且一般放在结构体成员的最后一个
struct str
{
int arr[0];//并且不占一个结构体的大小,只有在动态申请内存的时候才会显现出来
};
//但是直接定义一个数组大小为0这是不被允许的
int main()
{
int arr[0];//不合法
return 0;
}
以下是柔性数组的具体使用
#include
#include
#include
typedef struct str
{
int data;
int arr[];//写成int arr[0]也可以
}str;
int main()
{
str* p = (str*)malloc(sizeof(str) + (10 * sizeof(int)));//后面的“10 * sizeof(int)”就是“柔性数组”的部分,这样子保证了空间的连贯性(柔性数组开辟空间是紧跟着原有结构体的)
if (!p) exit(-1);
p->data = 0;
for (int i = 0; i < 10; i++)
{
p->arr[i] = i * i;
}
printf("p->data == %d\n", p->data);
for (int j = 0; j < 10; j++)
{
printf("%d ", p->arr[j]);
}
free(p);
p = NULL;
return 0;
}
柔性数组不能理解成一个指针,而应该理解为一个符号/象征,这样使得结构体的大小是可变的。
那为什么说保证了空间的连贯性呢?因为如果不使用柔性数组,那么就会写出下面这样的代码
#include
#include
#include
typedef struct str
{
int data;
int* arr;
//int arr[];
}str;
int main()
{
str* p = (str*)malloc(sizeof(str));
if (!p) exit(-1);
p->arr = (int*)malloc(sizeof(int) * 10);
if (!(p->arr)) exit(-1);
p->data = 0;
for (int i = 0; i < 10; i++)
{
p->arr[i] = i * i;
}
printf("p->data == %d\n", p->data);
for (int j = 0; j < 10; j++)
{
printf("%d ", p->arr[j]);
}
free(p->arr);
free(p);
p = NULL;
return 0;
}
这样的代码也不是不可以(事实上这种做法是最多的,因为柔性数组的概念还不够完全普及),只是对比使用柔性数组,其代码错误概率会更加高,因为需要malloc两次并且free两次,次数越多,错误率越高。
以后补充
联合体是一种对数据存储的解决方案。
联合体的内存是共用的,并且大小端对它的影响是比较大的。
联合体大小不能小于最大成员的大小,并其起始位置和其所有成员的起始地址都是一样的(在数值上),即:每一个变量从最低地址处一起共用同一块内存。
#include
#include
#include
typedef union Un
{
double i;
char j;
int k;
char o;
char w;
float u;
}Un;
int main()
{
Un a;
printf("%zd\n", sizeof(a));
printf("%p\n", &a);
printf("%p\n", &(a.i));
printf("%p\n", &(a.j));
printf("%p\n", &(a.k));
printf("%p\n", &(a.o));
printf("%p\n", &(a.w));
printf("%p\n", &(a.u));
}
通过联合体可以写出下面这样的“奇怪”的代码(小端模式)
#include
#include
#include
typedef union Un
{
int i;
char j;
}Un;
int main()
{
Un a;
a.i = 1;
printf("%d", a.j);//输出1的话就可以判断运行机器是小端机器
}
enum的含义就是“枚举”,可以创建枚举常量,具体使用如下:
enum color
{
RED;
YELLOR;
BLUE;
};
尽管枚举类型可以理解为int类型,都是还是有区别的!枚举定义的变量初始化和赋值最好还是取结构体内部定义的内容,而不应使用整型直接初始化或赋值。
枚举变量使得整型变量携带上一些文本信息(具有自描述性),这些信息是供人类阅读的(也就是说提高了代码的可读性),对计算机来说是没有区别的。
如果使用宏也可以,但是对比enum来说,枚举常量会更加方便,而且会做语法检查,因此在大型项目中枚举的使用率还挺高的。
用来给类型重命名,并不是创建一个类型,typedef的使用可以规范化代码和简化类型名,但是注意过多的使用有可能造成阅读困难。
//C语言定义数组的方式其实很奇怪,比如int arr[3]数组的类型是“int[3]”,指向函数void fun(void)的指针p的类型为“void(*)(void)”,但是按照定义变量的规则,正常来讲应该是“类型+变量名”的顺序,而上述提到的类型都是将变量名杂糅在类型中,而typedef就可以避免这些现象
#include
typedef int intarr[3];//这里的intarr不是一个数组了,而是一个数组类型,这个类型是int[3]
int main()
{
intarr arr = { 1, 2, 3 };
for (int i = 0; i < 3; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
typedef和宏的对比最大的地方在于连续定义多个变量的时候,下面代码揭示了typedef是重命名一个类型,而不是直接替换类型
typedef int* ip;
ip a, b;//a,b都是int*类型
#define int* IP;
IP c, d;//c是int*类型,但是d是int类型
在其他关键字对两者进行修饰的时候也会有所区别
#include
#define INT_1 int
typedef int INT_2;
int main()
{
unsigned INT_1 a = 10;//合法
//unsigned INT_2 b = 10;//不合法,typedef重新定义的变量类型无法这么做
const INT_2 c = 10;//合法
printf("%d %d", a, c);
return 0;
}
另外typedef重命名的类型中,是没有办法加入存储类关键字,因为typedef是存储类关键字,而两个及以上存储类关键字没有办法放在一起使用的(这在最后的总结也有提到)
typedef static int s_int;//不合法
在VS2022中报错是“指定了一个以上的存储类”
任何一个C程序再运行的时候i都会打开标准输入、标准输出、标准错误这三个流。
printf的返回值是输出到屏幕上的字符个数(这个时候就会发现printf输出到屏幕上的东西都是字符!!!这也是为什么叫格式化函数的原因,将数据格式化输出到屏幕,而键盘和显示器都可以叫“字符设备”)。
计算机的删除数据并不是重置数据为某个数或者清空数据,而是直接设置该数据无效,所以有的时候我们可以看到删除数据的速度要比传入数据快很多。
此我们应尽量使用标准C的编写方式才能使得代码具有强跨平台的特性。
内存中的编址是不需要开辟空间存储的,是通过硬件电路的方式对内存进行编址。
int x;
x = 100;//x的空间,侧重x变量的属性,左值
int y = x;//x的内容,侧重数据的属性,右值
//任何的变量名,在不同的应用场景中有可能代表不同的含义
一是翻译原因,二也有可能是左值和右值的原因。
对任何变量取地址“&”都是从最低地址开始。
(类型相同)对指针进行解引用代表的就是指针所指向的目标。
C语言中任何函数参数都一定会形成临时变量,包括指针变量。
注意声明是没有开辟空间的,extern声明的变量只是声明。
整型 char short int long
浮点型 float double
有无符号 signed unsigned
自定义类型 struct union enum
空类型 void
循环控制 for do while break continue
条件语句 if else goto(无条件跳转语句)
开关语句 switch case default
返回语句 return
注意存储关键字是不能放在一起使用的
autoextern register static typedef
(typedef关键字也被分到存储关键字分类中,虽然看起来没有什么关系)
const sizeof volatile
由于目前最为常用的标准还是C90,C99的以后再补呈给您罢,祝君共勉