C语言32个关键字细节

目录

数据类型关键字

char(声明字符型变量或函数)

short(声明短整型变量或函数)

int(声明整型变量或函数)

long(声明长整型变量或函数)

signed(声明有符号类型变量或函数)

unsigned(声明无符号类型变量或函数)

float(声明浮点型变量或函数)

double(声明双精度变量或函数)

struct(声明结构体变量或函数)

 结构体变量的创建和初始化

 结构的特殊声明

结构体内存对齐

 结构体传参

 结构体实现位段

位段的内存分配

位段的跨平台问题

位段的应用

位段使⽤的注意事项 

union(声明共用体(联合)数据类型)

1 联合体类型的声明

联合体的特点 

enum:声明枚举类型

void(声明函数无返回值或无参数,声明无类型指针) 

 为何 void 不能定义变量

void修饰函数返回值和参数

 void 指针

void * 定义的指针变量可以进行运算操作吗 

控制语句关键字(12个)

1.循环控制(5个)(基本语法不细谈)

for  && do  &&  while  &&  break  &&  continue

 三种循环对应的死循环写法

2.条件语句(3个)

if && else(只谈关键)

goto

3.开关语句(3个)

switch && case && default

4.返回语句(1个)

return

存储类型关键字(5个)

auto

extern

register

static

typedef

其他关键字(3个)

const

sizeof

volatile



数据类型关键字

char(声明字符型变量或函数)

short(声明短整型变量或函数)

int(声明整型变量或函数)

long(声明长整型变量或函数)

signed(声明有符号类型变量或函数)

unsigned(声明无符号类型变量或函数)

float(声明浮点型变量或函数)

double(声明双精度变量或函数)

以上这几类不去细谈,了解即可

重要的是在内存中的存储方式

以下两篇文章细谈了整形和浮点型的存储:大家可以花时间了解一下,对理解内存存储有重要帮助数据的存储-CSDN博客文章浏览阅读34次。我们之前讲过一个变量的创建是要在内存中开辟空间的,空间的大小是根据不同的类型而决定的。那么,数据在所开辟内存中到底是如何存储的呢?我们知道,编译器为a分配四个字节的空间。那如何存储呢?首先,对于有符号数,一定要能表示该数据是 正数还是负数。所以我们一般用最高比特位来进行充当符号位。原码、反码、补码计算机中的有符号数有三种表示方法,即原码、反码和补码。https://blog.csdn.net/2301_80322352/article/details/134966556浮点数的存储-CSDN博客文章浏览阅读27次。常见的浮点数: 3.14159、1E10等,浮点数家族包括: float、 double、 long double类型。浮点数表示的范围: float.h 中定义上面的代码中,num 和*pFloat在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。根据国际标准IEEE (电气和电子工程协会) 754, 任意一个二进制浮点数V可以表示成下面的形式:● (- 1)S表示符号位,当S=0, V为正数;https://blog.csdn.net/2301_80322352/article/details/134979599

struct(声明结构体变量或函数)

结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

例如描述⼀个学⽣:
struct Stu
{
   char name[20];//名字
   int age;//年龄
   char sex[5];//性别
   char id[20];//学号
}; //分号不能少

 结构体变量的创建和初始化

#include <stdio.h>
struct Stu
{
   char name[20];//名字
   int age;//年龄
   char sex[5];//性别
   char id[20];//学号
};
int main()
{
   //按照结构体成员的顺序初始化
   struct Stu s = { "张三", 20, "男", "20230818001" };
   printf("name: %s\n", s.name);
   printf("age : %d\n", s.age);
   printf("sex : %s\n", s.sex);
   printf("id : %s\n", s.id);

   //按照指定的顺序初始化
   struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "女" };
   printf("name: %s\n", s2.name);
   printf("age : %d\n", s2.age);
   printf("sex : %s\n", s2.sex);
   printf("id : %s\n", s2.id);
   return 0;
}

 结构的特殊声明

在声明结构的时候,可以不完全的声明

//匿名结构体类型
struct
{
   int a;
   char b;
   float c;
}x;

struct
{
   int a;
   char b;
   float c;
}a[20], *p;

 上⾯的两个结构在声明的时候省略掉了结构体标签(tag)。

那么问题来了?

//在上⾯代码的基础上,下⾯的代码合法吗?

p  =  &x;

警告:

编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。 匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。如果不是场景需求,尽量避免匿名的结构体类型定义。

结构体内存对齐

 我们已经掌握了结构体的基本使⽤了。

现在我们深⼊讨论⼀个问题:计算结构体的⼤⼩。

⾸先得掌握结构体的对⻬规则:

1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处

2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。 对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。

VS 中默认的值为 8

Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩

3. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的 整数倍。

4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构 体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

可能单独看这一串话会有点晦涩难懂,我们接下来看一个简单的例子就明白了

struct S1
{
   char c1;
   int i;
   char c2;
};

printf("%d\n", sizeof(struct S1));

这时大部分没有了解结构体内存对齐的人都会认为输出结果为6,因为两个char就是两个字节,int四个字节,加在一起就是6个字节

然而真相就是如此吗

可以看到结果为12,和我们的预期不符,这是为什么呢,我们来画个图就能理解内存对齐的方式了

 大家还可以看看这个代码

 为什么结构体内容明明一样却大小不同呢

这个时候你就可以之间尝试画图理解

只有自己经过尝试才能更加深刻的了解到内存对齐的方式

为什么存在内存对⻬?

⼤部分的参考资料都是这样说的:

1. 平台原因 (移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定 类型的数据,否则抛出硬件异常。

2. 性能原因: 数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。大家看个图就能大概理解了

总体来说:结构体的内存对⻬是拿空间来换取时间的做法。

那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到

1.让占⽤空间⼩的成员尽量集中在⼀起

2.修改默认对⻬数:#pragma 这个预处理指令,可以改变编译器的默认对⻬数。

#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
   char c1;
   int i;
   char c2;
};

#pragma pack()//取消设置的对⻬数,还原为默认

int main()
{
   //输出的结果是什么?
 printf("%d\n", sizeof(struct S));
 return 0;
}

结构体在对⻬⽅式不合适的时候,我们可以⾃⼰更改默认对⻬数。

 结构体传参

#include<stdio.h>
struct S
{
	int data[1000];
	int num;
};
struct S s = { {1,2,3,4}, 1000 };

//结构体传参
void print1(struct S s)
{
	printf("%d\n", s.num);
}

//结构体地址传参
void print2(struct S* ps)
{
	printf("%d\n", ps->num);
}
int main()
{
	print1(s); //传结构体
	print2(&s); //传地址
	return 0;
}

上⾯的 print1 和 print2 函数哪个好些?

答案是:⾸选print2函数。

原因: 函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下 降。

结论: 结构体传参的时候,要传结构体的地址。

 结构体实现位段

结构体讲完就得讲讲结构体实现 位段 的能⼒

什么是位段

位段的声明和结构是类似的,有两个不同: 1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以 选择其他类型。 2. 位段的成员名后边有⼀个冒号和⼀个数字。

//举例
#include<stdio.h>
struct A
{
   int _a:2;
   int _b:5;
   int _c:10;
   int _d:30;
};

int main()
{
   printf("%d\n",sizeof(struct A) );
}

 A就是⼀个位段类型。

那位段A所占内存的⼤⼩是多少?

 为什么结果时8呢?

2+5+10+30 = 47,那分6字节,也就是48比特不就够了吗.

接下来我们来看看位端的内存分配

位段的内存分配

1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型

2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。

3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。

通过上面的话,我们可以来看看为什么结果时8了

首先,系统会先开辟4个字节,不够了再继续开辟,所以当4个字节开辟完后,发现不够存储,会再继续开辟4个字节,所以总共就是8个字节

//⼀个例⼦
struct S
{
   char a:3;
   char b:4;
   char c:5;
   char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
//空间是如何开辟的?

到底是a和b用完一个字节后,系统再开辟一个字节,c是先用a和b用剩下的一个bit位再去新开辟的空间里使用,还是直接就使用新开辟的空间呢 

口水话就不多说了,大家直接看图理解,看图可发现,c是直接使用了下个新开辟的空间,而没有用上一次空间剩余下来的空间,所以可以直接得出结论:当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,第二个位段会直接使用新开辟的空间。

所以真的就是这样吗?

我们来聊聊位段的跨平台问题

位段的跨平台问题

 1. int 位段被当成有符号数还是⽆符号数是不确定的。

2. 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会 出问题。

3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

4. 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃 剩余的位还是利⽤,这是不确定的。

总结: 跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。所以上面的图只是再vs下测试得出来的结论,并不是所有的编译器都是如此,大家要有清晰的认识

位段的应用

下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这⾥ 使⽤位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较下⼀些,对网络 的畅通是有帮助的。 

位段使⽤的注意事项 

位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位 置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。 所以不能对位段的成员使⽤&操作符,这样就不能使⽤scanf直接给位段的成员输⼊值,只能是先输⼊ 放在⼀个变量中,然后赋值给位段的成员。

#include<stdio.h>
struct A
{
   int _a : 2;
   int _b : 5;
   int _c : 10;
   int _d : 30;
};
int main()
{
   struct A sa = {0};
   scanf("%d", &sa._b);//这是错误的

   //正确的⽰范
   int b = 0;
   scanf("%d", &b);
   sa._b = b;
 return 0;
}

union(声明共用体(联合)数据类型)

1 联合体类型的声明

像结构体⼀样,联合体也是由⼀个或者多个成员构成,这些成员可以不同的类型。 但是编译器只为最⼤的成员分配⾜够的内存空间。联合体的特点是所有成员共⽤同⼀块内存空间。所 以联合体也叫:共⽤体。

给联合体其中⼀个成员赋值,其他成员的值也跟着变化。

联合体的特点 

联合的成员是共⽤同⼀块内存空间的,这样⼀个联合变量的⼤⼩,⾄少是最⼤成员的⼤⼩(因为联合 ⾄少得有能⼒保存最⼤的那个成员)。

 

代码1输出的三个地址⼀模⼀样,代码2的输出,我们发现将i的第4个字节的内容修改为55了。 我们仔细分析就可以画出,un的内存布局图。

 

相同成员的结构体和联合体对⽐ 

我们再对⽐⼀下相同成员的结构体和联合体的内存布局情况。

联合体大小的计算

联合的大小至少是最⼤成员的大小。

当最⼤成员大小不是最⼤对⻬数的整数倍的时候,就要对⻬到最⼤对⻬数的整数倍。

 

使⽤联合体是可以节省空间的,举例: 

 比如,我们要搞⼀个活动,要上线⼀个礼品兑换单,礼品兑换单中有三种商品:图书、杯⼦、衬衫。 每⼀种商品都有:库存量、价格、商品类型和商品类型相关的其他信息。

图书:书名、作者、页数

杯⼦:设计

衬衫:设计、可选颜⾊、可选尺⼨

那我们不耐⼼思考,直接写出⼀下结构:

struct gift_list
{
   //公共属性
   int stock_number;//库存量
   double price; //定价
   int item_type;//商品类型

   //特殊属性
   char title[20];//书名

   char author[20];//作者
   int num_pages;//⻚数

   char design[30];//设计
   int colors;//颜⾊
   int sizes;//尺⼨
};

上述的结构其实设计的很简单,⽤起来也⽅便,但是结构的设计中包含了所有礼品的各种属性,这样 使得结构体的⼤⼩就会偏⼤,⽐较浪费内存。因为对于礼品兑换单中的商品来说,只有部分属性信息 是常⽤的。⽐如:

商品是图书,就不需要design、colors、sizes。

所以我们就可以把公共属性单独写出来,剩余属于各种商品本⾝的属性使⽤联合体起来,这样就可以 介绍所需的内存空间,⼀定程度上节省了内存。 

struct gift_list
{
   int stock_number;//库存量
   double price; //定价
   int item_type;//商品类型

 union
 {
    struct
    {
      char title[20];//书名
      char author[20];//作者
      int num_pages;//⻚数
    }book;
    struct
    {
      char design[30];//设计
    }mug;
    struct
    {
      char design[30];//设计
      int colors;//颜⾊
      int sizes;//尺⼨
    }shirt;

 }item;
};

enum:声明枚举类型

枚举顾名思义就是⼀⼀列举。

把可能的取值⼀⼀列举。

⽐如我们现实⽣活中: ⼀周的星期⼀到星期⽇是有限的7天,可以⼀⼀列举 性别有:男、⼥、保密,也可以⼀⼀列举 ⽉份有12个⽉,也可以⼀⼀列举 三原⾊,也是可以一 一列举

这些数据的表示就可以使⽤枚举了

enum Day//星期
{
   Mon,
   Tues,
   Wed,
   Thur,
   Fri,
   Sat,
   Sun
};
enum Sex//性别
{
   MALE,
   FEMALE,
   SECRET
};
enum Color//颜⾊
{
   RED,
   GREEN,
   BLUE
};

以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。 { }中的内容是枚举类型的可能取值,也叫 枚举常量 。 这些可能取值都是有值的,默认从0开始,依次递增1,当然在声明枚举类型的时候也可以赋初值。  

枚举类型的优点

为什么使⽤枚举?

我们可以使⽤ #define 定义常量,为什么⾮要使⽤枚举? 枚举的优点:

1. 增加代码的可读性和可维护性

2. 和#define定义的标识符⽐较枚举有类型检查,更加严谨。

3. 便于调试,预处理阶段会删除 #define 定义的符号

4. 使⽤⽅便,⼀次可以定义多个常量

5. 枚举常量是遵循作⽤域规则的,枚举声明在函数内,只能在函数内使⽤

那是否可以拿整数给枚举变量赋值呢?在C语⾔中是可以的,但是在C++是不⾏的,C++的类型检查比较严格。 

void(声明函数无返回值或无参数,声明无类型指针) 

//void是否可以定义变量
#include <stdio.h>
int main()
{
  void a;
  return 0;
}
//在vs2013和Centos 7,gcc 4.8.5下都不能编译通过

 为何 void 不能定义变量

定义变量的本质:开辟空间

而void作为空类型,理论上是不应该开辟空间的,即使开了空间,也仅仅作为一个占位符看待 所以,既然无法开辟空间,那么也就无法作为正常变量使用,既然无法使用,编译器干脆不让他定义变量。

在vs2013中,sizeof(void)=0

在Linux中,sizeof(void)=1(但编译器依旧理解成,无法定义变量)

void修饰函数返回值和参数

//场景1
//void用来作为函数返回值
#include <stdio.h>
void show()
{
  printf("no return value!\n");
}
int main()
{
  show();
  return 0;
}
//如果自定义函数,或者库函数不需要返回值,那么就可以写成void
//那么问题来了,可以不写吗?不可以,自定义函数的默认返回值是int(这个现场验证)
//所以,没有返回值,如果不写void,会让阅读你代码的人产生误解:他是忘了写,还是想默认int?

 结论:void作为函数返回值,代表不需要,这里是一个"占位符"的概念,是告知编译器和给阅读源代码的工程师看的

//场景2
//void 作为函数参数
//如果一个函数没有参数,我们可以不写, 如test1()
#include <stdio.h>
int test1() //函数默认不需要参数
{
  return 1;
}
int test2(void) //明确函数不需要参数
{
  return 1;
}
int main()
{
  printf("%d\n", test1(10)); //依旧传入参数,编译器不会告警或者报错
  printf("%d\n", test2(10)); //依旧传入参数,编译器会告警(vs)或者报错(gcc)
  return 0;
}
//结论:如果一个函数没有参数,将参数列表设置成void,是一个不错的习惯,因为可以将错误明确提前发现
//另外,阅读你代码的人,也一眼看出,不需要参数。相当于"自解释"。
//当然,如果不习惯也不勉强。

 void 指针

//void不能定义变量,那么void*呢?
#include <stdio.h>
int main()
{
  void *p = NULL; //可以
  return 0;
}
//为什么void*可以呢?因为void*是指针,是指针,空间大小就能明确出来

//场景
//void* 能够接受任意指针类型
#include <stdio.h>
int main()
{
  void *p = NULL;
  int *x = NULL;
  double *y = NULL;
  p = x; //虽然类型不同,但是编译器并不报错
  p = y; //同上
  return 0;
}
//反过来,在vs/gcc中也没有报错。书中编译器很老了,我们严重不推荐(C语言深度解剖)

//结论:但是我们依旧认为,void*的作用是用来接受任意指针类型的。这块在后面如果想设计出通用接口,很有用

//比如:

//void * memset ( void * ptr, int value, size_t num );

void * 定义的指针变量可以进行运算操作吗 

//在vs2013中
#include <stdio.h>
int main()
{
  void *p = NULL;
  p++; //报错
  p += 1; //报错
  return 0;
}
//在gcc4.8.5中
#include <stdio.h>
int main()
{
  void *p = NULL; //NULL在数值层面,就是0
  p++; //能通过
  printf("%d\n", p); //输出1
  p += 1; //能通过
  printf("%d\n", p); //输出2
  return 0;
}
//为什么在不同的平台下,编译器会表现出不同的现象呢?
根本原因是因为使用的C标准扩展的问题。具体阅读书。(C语言深度解剖)
我这里有问题:
对指针++具体是怎么++?
为何gcc中,输出结果是 1 和 2 呢?
本质和不同平台,看待void空间大小相关。
补充内容:
GNU计划,又称革奴计划,是由Richard Stallman(理查德·斯托曼)在1983年9月27日公开发起的。它的目是创建一套完全自由的操作系统。它在编写linux的时候自己制作了一个标准成为 GNU C标准。ANSI 美国国家标准协会它对C做的标准ANSI C标准后来被国际标准协会接收成为 标准C 所以 ANSI C 和标准C是一个概念,总体来说现在linux也支持标准C,以后标准C可以跨平台,而GUN c 一般只在linux c下应用。--来自百度
Linux 上可用的 C 编译器是 GNU C 编译器,它建立在自由软件基金会的编程许可证的基础上,因此可以自由发布。 GNUC 对标准 C 进行一系列扩展,以增强标准 C 的功能。--来自百度
一句话,大部分编译器是标准C,而Linux下是扩展C,Linux平台也能保证标准C的运行

控制语句关键字(12个)

1.循环控制(5个)(基本语法不细谈)

for  && do  &&  while  &&  break  &&  continue

//while
条件初始化
while(条件判定){
    //业务更新
    条件更新
}

//for
for(条件初始化; 条件判定; 条件更新){
  //业务代码
}

//do while
条件初始化
do{
   条件更新
}while(条件判定);

 三种循环对应的死循环写法

while(1){
}

for(;;){
}

do{

}while(1);
//书中代码,优化后(C语言深度解剖)

//这个代码输出的时候,会有什么问题?
#include <stdio.h>
int main()
{
  while (1){
     char c = getchar(); //读取字符函数
     if ('#' == c){
        break;
     }
     printf("echo: %c\n", c); 
  }
  return 0;
}
//测试用例1:break:终端输入abcd#1234
//测试用例2:continue:终端输入abcd#1234
#include <stdio.h>
int main()
{
  while (1){
    char c = getchar(); 
    if ('#' == c){
       break;           //测试用例1
      //continue;       //测试用例2
    }
    printf("echo: %c\n", c);
  }
  return 0;
}

循环必备的三要素(循环条件初始化,循环条件判定,循环条件更新) 

注意:在while和do while中,continue是跳到条件判断处

           在for中,continue是跳到条件更新处

2.条件语句(三个)

if && else(只谈关键)

1. if语句的执行流程永远都是先执行()内的语句,得出bool值再判断真假

2. 0为假,非0为真

0为假,1为真这个说法并不准确。

3. if else匹配原则:
else匹配采取就近原则,推荐每次写if 或者else都带上{ }。

4. if后加分号:

  这个问题初学者很容易犯,因为系统并不报错,

;就代表if执行的是一个空语句;所以从语法来讲并不出错

5. ==误写成=,误写后,如果右边对于的是一个非0常量,if就会永远成立;

goto

基本使用

//使用goto模拟实现循环
#include <stdio.h>
int main()
{
  int i = 0;
  START:
  printf("[%d]goto running ... \n", i);
  Sleep(1000);
  ++i;
  if (i < 10){
  goto START;    //永远都会跳到上面的START
}
  printf("goto end ... \n");
  return 0;
}

 不要对goto有偏见,在一些特殊场景下goto有奇效

像Linux内核源代码中充满了大量的goto,只能说我们目前,或者很多公司的业务逻辑不是那么复杂,所有大多数人都会对goto有偏见

3.开关语句(3个)

switch && case && default

switch(整型变量/常量/整型表达式){
  case var1:
      break;
  case var2:
      break;
  case var3: 
      break;
    default:
      break;
}

 已经有if else为何还要switch case

switch语句也是一种分支语句,常常用于多分支的情况。这种多分支,一般指的是很多多分支,而且判定条件主要以整型为主,如:

输入1,输出星期一

输入2,输出星期二

输入3,输出星期三

输入4,输出星期四

输入5,输出星期五

输入6,输出星期六

输入7,输出星期日

如果写成 if else 当然是可以的,不过比较麻烦

#include <stdio.h>
int main()
{
  int day = 1;
  switch (day){
  case 1:
     printf("星期一\n");
     break;
  case 2:
     printf("星期二\n");
     break; 
  case 3:
     printf("星期三\n");
     break;
  case 4:
     printf("星期四\n");
     break;
  case 5:
     printf("星期五\n");
     break;
  case 6:
     printf("星期六\n");
     break;
  case 7:
     printf("星期日\n");
     break;
 default:
     printf("bug!\n");
     break;
 }

 return 0;
}

case的作用是什么?break在switch中的作用是什么?

case本质本质是进行判定功能

break本质其实是进行分支功能

switch本身是没有像if else一样拥有判断,分支功能的

 如果多个不同case匹配,想执行同一个语句,推荐做法:

#include <stdio.h>
int main()
{
  int day = 6;
  switch (day){
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
       printf("周内\n");
       break;
    case 6:
    case 7:
       printf("周末\n");
       break;
   default:
       printf("bug!\n");
       break;
   }
   return 0;
}

结论:case之后,如果没有break,则会依次执行后续有效语句,直到碰到break

case后面的值有什么要求吗? 

switch(m) && case n
//其中m 和 n必须是什么类型变量或者表达式?
//case 语句后面是否可以是const修饰的只读变量呢?不行
#include <stdio.h>
int main()
{
  const int a = 10;
  switch (a){
     case a:      //不行
        printf("hello\n");
        break;
    default:
        break;
   }
   return 0;
}

case default必须要按顺序放吗?

不是必须,但尽量按顺序放,除非特殊场景。

//default可以放在switch内任意处

 总结

4.返回语句(1个)

return

//如何正确理解这段代码
#include <stdio.h>
char* show()
{
  char str[] = "hello bit";
  return str;
}
int main()
{
  char *s = show();
  printf("%s\n", s);
  return 0;
}
//如何理解栈帧销毁,计算机中所谓的删除数据究竟是在做什么?

//返回值临时变量接收的本质
#include <stdio.h>
int test()
{
  int a = 10;
  return a; //既然a是函数定义的变量,具有临时性,那么这个临时变量在函数退出的时候,应该被释放
}
int main()
{
  int a = test();
  printf("%d\n", a);
  //test(); //没有变量接收返回值的时候,有返回值吗?
  return 0;
}

 

//通过查看汇编可以看到,对于一般内置类型,寄存器eax可以充当返回值的临时空间
//回答结尾问题:通过查看汇编方式,理解返回值
#include <stdio.h>
//void test()
int test()
{
  return 1;
}
int main()
{
  int x = test();
  //test();
  return 0;
}

return是通过寄存器返回值的

//所以如何正确理解这段代码
#include <stdio.h>
char* show()
{
  char str[] = "hello bit";
  return str;
}
int main()
{
  char *s = show();
  printf("%s\n", s);
  return 0;
}

 该程序打印出来的为什么会是乱码呢?(可自行测试)

因为return返回的时候,show函数中的str这个临时数组会被自动释放掉(临时变量具有临时性质的特征),但计算机是不会真正意义上的清空我们的数据,(当然,实际情况会把我们的空间进行一定程度的重新启动,也就是初始化,清空什么的)但现在只需要理解计算机并不会立马清空数据即可,所以str释放掉后,我们依旧可以通过监视看到对应的字符串,但是为什么通过printf函数打印后就是乱码呢,原因在于任何一个函数被调用的时候都会形成一个栈帧,main函数也是函数,所以,main函数本身也有栈帧结构,所谓栈帧结构就是再栈上为对应函数开辟一段小的空间,所以当我们调用printf函数时,就会在main函数的下层,为printf函数形成栈帧,这样就会覆盖show函数曾经我们建立好的栈帧结构,里面的数据也就全部被清空了,所以最终打印出来的是乱码。

存储类型关键字(5个)

auto

如何使用:一般在代码块中定义的变量,即局部变量,默认都是auto修饰的,不过一般省略 默认的所有变量都是auto吗?不是,一般用来修饰局部变量 中断一下:后面我们所到的,局部变量,自动变量,临时变量,都是一回事。我们统称局部变量 

#include <stdio.h>
int main()
{
  for (int i = 0; i < 10; i++){
     printf("i=%d\n", i);
     if(1)
       {
         auto int j = 0;   //自动变量
         printf("before: j=%d\n", j);
         j += 1;
         printf("after : j=%d\n", j);
       }
    }
   return 0;
}

i用auto修饰可以吗?去掉的auto可以吗?

答案是都可以,不过现在定义变量其实前面都已经有隐藏的auto了

结论:已经很老,基本用不使用

extern

可以看到,明明在test.c中定义的g_val,为什么main.c也能用呢,原因就在于 extern修饰后,扩大了g_val的作用域,能让其他文件也能看到它

extern我们这里只关注代码风格:

大家以后在多文件情况下是写代码时,在头文件中不管是变量还是函数声明,都要带上extern,虽然有时候没有带上,编译器也不会报错,但带上好处多多,我就不细谈了。

register

其实,CPU主要是负责进行计算的硬件单元,但是为了方便运算,一般第一步需要先把数据从内存读取到CPU内,那 么也就需要CPU具有一定的数据临时存储能力。注意:CPU并不是当前要计算了,才把特定数据读到CPU里面,那样 太慢了。 所以现代CPU内,都集成了一组叫做寄存器的硬件,用来做临时数据的保存。

距离CPU越近的存储硬件,速度越快

寄存器存在的本质

在硬件层面上,提高计算机的运算效率。因为不需要从内存里读取数据。

register 修饰变量

尽量将所修饰变量,放入CPU寄存区中,从而达到提高效率的目的。(并不是说只要修饰了,就一定会放到CPU寄存区中)

那么什么样的变量,可以采用register呢?

1. 局部的(全局会导致CPU寄存器被长时间占用) 

2. 不会被写入的(写入就需要写回内存,后续还要读取检测的话,register的意义在哪呢?)

3. 高频被读取的(提高效率所在)

4. 如果要使用,请不要大量使用,因为寄存器数量有限

这里除了上面的,再有一点,就是register修饰的变量,不能取地址(因为已经放在寄存区中了嘛,地址是内存相关的概念)

我的意见:该关键字,不用管,因为现在的编译器,已经很智能了,能够进行比人更好的代码优化。 早期编译器需要人为指定register,来进行手动优化,现在不需要了

static

.h:我们称之为头文件,一般包含函数声明,变量声明,宏定义,头文件等内容(header) 

.c: 我们称之为源文件,一般包含函数实现,变量定义等 (.c  : c语言) 

//test.h
#pragma once //防止头文件被重复包含,当前只需要记住,后面会无数次用
#include <stdio.h>
//test.c
#include "test.h" //""包含头文件,目前只需要知道是自己写的头文件,就用""包含即可
//main.c
#include "test.h" //同上
int main()
{
   printf("hello files!\n");
   return 0;
}

全局变量和函数的两个结论

1. 全局变量,是可以跨文件,被访问的。

2. 全局函数,是可以跨文件,被访问的

修饰变量

1. 修饰全局变量,该全局变量只能在本文件内被使用。

//总结:static修饰全局变量,影响的是作用域的概念,函数类似。而生命周期是不变的.

2. 修饰局部变量
void fun1()
{
  int i = 0;
  i++;
  printf("no static: i=%d\n", i);
}
void fun2()
{
  static int i = 0;
  i++;
  printf("has static: i=%d\n", i);
}
int main()
{
  for (int i = 0; i < 10; i++){
  fun1(); 
  fun2();
}
  return 0;
}
//结论:static修饰局部变量,变量的生命周期变成全局周期。(作用域不变)

有些误导性  

第一个作用:修饰变量。变量又分为全局变量和局部变量,但它们都在内存的静态区。

关于C存储布局,我后面会在函数专题整体来讲 

 static只能让变量的生命周期变为全局周期,作用域是不变的。

typedef

typedef可以让我们在面对冗长的类型命名方面,可以让我们解脱出来。

但是不要一昧的把所以类型都重命名,目前我只推荐使用结构体的时候使用typedef,其他场景都不太推荐

typedef和宏的区别

#define是完成替换功能,而typedef是定义新的类型 ,会对后面的所有变量全部起效

为什么是新的类型呢,可以举个例子

可以看到typedef不能和其他关键字一起修饰变量,哪怕它们之前是可以一起的。

 那这个又是什么原因呢,从报错信息来看,我们可以大概知道static和typedef都是存储类关键字,C语言中不允许同时出现两个存储类型的关键字。

其他关键字(3个)

const

//const修饰变量
#include <stdio.h>
int main()
{
  const int i = 10;
  i = 20; //报错,代表变量i不可以直接被修改
  return 0;
}
//const修饰变量真的不能被修改吗?
#include <stdio.h>
int main()
{
  const int i = 10;
  int *p = (int*)&i;
  printf("before: %d\n", i);
  *p = 20;
  printf("after: %d\n", i);
  return 0;
}

//结论:const修饰的变量并非是真的不可被修改的常量

//那const修饰变量,意义何在?

1. 让编译器进行直接修改式检查

2. 告诉其他程序员(正在改你代码或者阅读你代码的)这个变量后面不要改哦。也属于一种“自描述”含义

//const修饰的变量,可以作为数组定义的一部分吗?
#include <stdio.h>
int main()
{
  const int n = 100;
  int arr[n];
  return 0;
}
//在vs2013(标准C)下直接报错了,但是在gcc(GNU扩展)下,可以
//但我们一切向标准看齐,不可以。

 来看一下const在不同位置有何作用

#include<stdio.h>
int main()
{
   int a = 0;
   const int *p = &a;         //表示p指向的空间内容不可更改 
   int* const p = &a;         //表示p的内容不可更改
   const int * const p = &a;  //表示p所指向的空间和p自身都不可更改
   return 0;
}

sizeof

自定义类型的大小也是可以用sizeof去求的 

 

#include <stdio.h>
int main()
{
	int a = 0;

	printf("%zd\n", sizeof(a));  //正确
	printf("%zd\n", sizeof(int)); //正确
	printf("%zd\n", sizeof a);   //正确
	printf("%zd\n", sizeof int);  //error
	return 0;
}

 sizeof是操作符用来求特定类型所占空间大小,而不是函数

volatile

心理建设

//保持内存可见性

//接下来,我在汇编角度,在Linux平台给大家对比演示一下加还是不加volatile的作用,让大家看明白。

//不过可能会有些难度,大家就看一下基本过程就行

[whb@VM-0-3-centos code]$ cat test.c
#include <stdio.h>
int pass = 1;
int main()
{
   while(pass){ //思考一下,这个代码有哪些地方,编译器是可以优化的。
}
  return 0;
}

[whb@VM-0-3-centos code]$ gcc test.c -O2 -g   //以O2级别进行代码优化
[whb@VM-0-3-centos code]$ objdump -S -d a.out > a.s   //对形成的a.out可执行程序进行优化
[whb@VM-0-3-centos code]$ vim a.s   //查看汇编代码

加volatile

[whb@VM-0-3-centos code]$ cat test.c
#include <stdio.h>
volatile int pass = 1; //加上volatile

int main()
{
   while(pass){
   }
   return 0;
}
[whb@VM-0-3-centos code]$ gcc test.c -O2 -g //以O2级别进行代码优化
[whb@VM-0-3-centos code]$ objdump -S -d a.out > aa.s //对形成的a.out可执行程序进行优化
[whb@VM-0-3-centos code]$ vim aa.s //查看汇编代码

 

结论: volatile 忽略编译器的优化,保持内存可见性 

其他问题

const volatile int a = 10;

//在vs2013和gcc 4.8中都能编译通过

//const是在编译期间起效果

//volatile在编译期间主要影响编译器,形成不优化的代码,进而影响运行,故:编译和运行都起效果。

const要求你不要进行写入就可以。volatile意思是你读取的时候,每次都要从内存读。 两者并不冲突。

虽然volatile就叫做易变关键字,但这里仅仅是描述它修饰的变量可能会变化,要编译器注意,并不是它要求对应变量必须变化!这点要特别注意。 

C语言中32个关键字也就讲完了,如果大家觉得还不错,对您有帮助的话可以点赞+关注哦!! 

  • 18
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值