目录
auto(自动变量)
- c语言中auto关键字是用来修饰自动变量的,是C语言中应用最广泛的一种类型。
- 在程序内定义的局部变量,如果没有被声明为其他类型的局部变量都是自动变量。
- 可以得出结论:没有被auto修饰的局部变量都是自动变量。
注:局部变量,自动变量,临时变量,都是一样的,统称局部变量。
//举例
#include <stdio.h>
auto int a = 0; //报错!全局变量不能被auto修饰
int main()
{
for (int i = 0; i < 2; 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;
}
从运行结果可以看出:
- 不管有没有被auto修饰的局部变量,都是自动变量!
- 也就是说,如果i用auto修饰并且去掉j的auto都是没问题的。
结论:
(1)一般在代码块定义的变量,即局部变量,默认都是auto修饰的,一般都是省略的
(2)该关键字已经很老,基本永不使用
(3)全局变量不是自动变量,也不能用auto修饰
register(寄存器)
- 在电脑中CPU主要是负责进行计算的硬件单元,但是为了方便运算,一般第一步需要先把数据从内存读取到CPU内,
- 那么也就需要CPU具有一定的数据临时存储能力。注意:CPU并不是当前要计算了,才把特定数据读到CPU里面,那样太慢了。
- 所以现代CPU内,都集成了一组叫做寄存器的硬件,用来做临时数据的保存
距离CPU越近的存储硬件,速度越快
寄存器的作用
本质上可以提高计算机计算的效率,因为省略了去内存读取数据这一步骤!
register 修饰变量
建议:
- 局部变量可以存储到寄存器中(全局会导致CPU寄存器被长时间占用)
- 不会被写入内存的变量(写入就需要写回内存)
- 高频被读取的(提高效率)
- 现在的编译器,已经很智能了,能够进行比人更好的代码优化,不像早期编译器需要人为指定register,来进行手动优化。
#include <stdio.h>
int main()
{
register int a = 0; //a变量被存储到寄存器中
int* p = &a; //报错
return 0;
}
报错的原因是:a已经被存储到寄存器中,不能被写入内存,因为地址是内存相关的概念!
extern(声明)
多文件
extern 声明变量
extern 声明函数
.c我们称之为源文件,一般包含函数实现,变量定义等
.h我们称之为头文件,一般包含函数声明,变量声明,宏定义,头文件等内容
//test.h
#pragma once //防止头文件被包含
#include <stdio.h> //c语言里面的头文件
extern int a; //声明变量,必须带上extern
extern void show(int n); //声明函数,建议带上extern
#define N 10 //宏定义
//main.c
#include "test.h" //""包含头文件,意思是包含自定义头文件
int main()
{
show(200); //调用函数
peintf("%d\n",a); //输出变量
return 0;
}
//test.c
#include "test.h" //意思跟上面一样
int a = 100; //对变量初始化
void show(int n) //函数的实现
{
printf("hello %d world\n",n);
}
为什么要声明头文件(.h)
结论:因为组织项目结构的时候,减少大型项目维护的成本!
为什么头文件里要带上#pragma once?
结论:防止头文件被重复包含,导致程序运行效率变慢。
为什么变量声明必须带上extern?
结论:因为变量未初始化的时候,编译器会默认初始化为随机值或0。
注:声明变量是不需要开辟空间的,而定义变量并赋值是需要开辟空间的。
结论:
1.全局变量,是可以跨文件,被访问的。
2.全局函数,是可以跨文件,被访问的。
3. 所有变量声明必须带上extern,建议函数声明也带上。
3.所有声明变量都不能初始化。
4.函数传参跨文件使用,建议在头文件声明函数和实现函数里写上参数。
注:在主函数外面定义或声明的都是全局变量。
static
想要了解static关键字的用法,请先移步到作用域和生命周期的文章c语言初识
static的作用
1.static修饰局部变量
局部变量生命周期变成全局变量生命周期,作用域不变。
2.static修饰全局变量
全局变量只能在本文件内使用。
3.static修饰函数
函数只能在本文件内使用,不能跨文件被访问(外部链接失效)。
注:static修饰全局变量影响的是作用域的概念,函数类似,生命周期不变。
static修饰局部变量
#include <stdio.h>
void test1()
{
int a = 0; //局部变量
a++;
printf("No static = %d ",a);
}
void test2()
{
static a = 0; //static修饰的局部变量
a++;
printf("Yes static = %d ",a);
}
int main()
{
int i = 0;
for(i = 0; i < 10; i++)
{
test1();
putchar('\n');
test2();
}
return 0
}
结论:
- 在函数体内定义局部变量具有临时性,函数结束,局部变量自动被释放。
- 被static修饰后的局部变量,被存储到静态区。
- 被static修饰的局部变量,具有全局生命周期性,出了代码块不会被释放,作用域不变,只能在代码块里定义的区域使用。
static修饰全局变量
//test.h
#include <stdio.h>
extern int a;
void fun(int num);
fun1();
//main.c
#include "test.h"
int main()
{
printf("%d\n",a); //报错,不能被外部文件直接访问
fun(100); //报错,不能被外部文件直接访问
fun1(); //通过间接访问的方式访问文件
return 0;
}
//test.c
#include "test.h"
static int a = 200; //static修饰全局变量
static void fun(int num) //static修饰函数
{
printf("%d\n",num);
}
void fun1()
{
fun(50);
}
结论:
- static修饰全局变量,该变量只能在本文件内使用,不能被外部其他文件直接访问。
- static修饰函数,该函数只能在本文件内使用,不能被外部其他文件直接访问。
- 可以通过间接访问的方式访问文件内部的变量和函数。
- 全局变量和函数都可以跨文件使用。
c语言基本数据类型
c语言内置类型如图所示:
c语言内置类型的本质是什么?
- 本质:在内存中开辟空间,用来存放数据。
- 但是定义一个变量,是需要类型的。那么,类型决定了:变量开辟空间的大小。
c语言类型 | 32位系统(字节) | 64位系统(字节) |
---|---|---|
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long int | 4 | 4 |
long long int | 8 | 8 |
float | 4 | 4 |
double | 8 | 8 |
指针 | 4 | 8 |
为什么要根据类型开辟空间?
- 因为开辟空间没有类型的话,就会造成不必要的浪费。
- 一个工程里,都不是一个程序在运行,还有很多其他程序也在运行。如果我把空间一次性开辟完,其他程序就会没有空间存放数据了。
- 为了满足不同的计算场景。
最后如果有人还没没听懂的话,举个例子:
比如你跟朋友一起吃蛋糕🎂,不是你自己一个人吃全部蛋糕🎂,还有其他人在呢!另外,你自己吃整个蛋糕🎂,一定能吃完吗?吃不完,就会造成浪费!所以说,要合理的分配蛋糕🎂,不要贪吃。
注:蛋糕🎂就是内存
变量名的命名规则
- 命名应当直观且可拼读,便于记忆与阅读。标识符采用英文单词或其组合,不允许使用拼音。
- 命名的长度合应符合:
“minlength&&max+information”
(最短长度,最大信息)- 避免名字中出现数字编号,如a1,a2等。除非是逻辑上或驱动开发时需要。
- 程序中不得出现仅靠大小写区分的相似标识符。 例如:(int x and int X)
- 要特别注意1和l这一对的区别,还有0和o的区别,很多人容易搞混。
- 所有宏定义,枚举常量,标识符全都用大写英文命名,用下划线分割。
- 命名规范:英文字母(a-z,A-Z),数字(0-9),中间连接可用下划线(_)。
sizeof
有什么用?
sizeof是计算变量在内存中开辟空间的大小(单位:字节(byte))。
sizeof是函数吗?
//举例
#include <stdio.h>
int main()
{
int a;
printf("%d\n"sizeof(int)); //4
printf("%d\n"sizeof(a)); //4
printf("%d\n"sizeof int); //报错
printf("%d\n"sizeof a); //4
//除了一个报错其他可以运行
return 0;
}
结论:
- sizeof如果是函数的话sizeof a 就会报错,函数调用必须带括号。
- sizeof int报错原因是:不能同时存在二个关键字。
- sizeof是关键字不是函数。
如何使用?
- sizeof在计算时,可以省略括号,但是括号内是数据类型就必须括起来.
- sizeof在计算时,括号里面的变量最好不要进行运算,因为结果都是原来的值.
- sizeof是在编译时求值,括号内进行运算是无效的.
//举例
#include <stdio.h>
int main()
{
int a = 0;
printf("%d\n", sizeof(a++)); //结果还是四个字节
return 0;
}
signed、unsigned与整形在内存的存储
有符号类型 vs 无符号类型
有符号类型 | 取值范围(32位平台) | 无符号类型 | 取值范围(32位平台) |
---|---|---|---|
signed char | [-128,127] | unsigned char | [0,255] |
signed short | [-32768,32767] | unsigned short | [0,65535] |
signed int | [-2147483648,2147483647] | unsigned int | [0,4294967295] |
signed long | [-2147483648,2147483647] | unsigned long | [0,4294967295] |
- 通用有符号类型取值范围公式:[-2n-1,2n-1-1]
- 通用无符号类型取值范围公式:[0,2n-1]
- 注:n为比特位(bit) =>> 1字节(byte)= 8比特位(bit)
原反补码
计算机中的有符号数有三种表示方法,即原码、反码和补码。
- 原码:将十进制按照正负数的形式翻译成二进制。
- 反码:原码的基础上,符号位(第一位)不变,其余依次按位取反。
- 补码:反码的基础上+1。
- 如果一个数据是正数,那么它的原反补都相同!
//举例
#include <stdio.h>
int main()
{
int a = 10;
//原反补码:0000 0000 0000 0000 0000 0000 0000 1010 =>10的二进制
printf("a=%d\n", a); //10
unsigned int b = 10;
//原反补码:0000 0000 0000 0000 0000 0000 0000 1010 =>10的二进制
printf("b=%u\n", b); //无符号类型输出 ==>10
int c = -1;
//原码:1000 0000 0000 0000 0000 0000 0000 0001
//反码:1111 1111 1111 1111 1111 1111 1111 1110 =>符号位不变,其余取反
//补码:1111 1111 1111 1111 1111 1111 1111 1111 =>反码+1
printf("c=%d\n", c); //-1
unsigned int d = -10;
//1000 0000 0000 0000 0000 0000 0000 1010
//1111 1111 1111 1111 1111 1111 1111 0101
//1111 1111 1111 1111 1111 1111 1111 0110
printf("d(u)=%u\n", d); //4294967286
printf("d(d)=%d\n", d); //-10
return 0;
}
为什么无符号输出unsigned int = -10的值等于429496…呢?
- 内存中开辟的空间里面存储的是-1的补码
- 无符号输出原反补码相等,不用转换为原码打印
为什么无符号类型可以存储负数?
- 内存中开辟的空间是用来存储数据的不管是正负数都可以存储,不要被类型给迷惑了
- 但是输出看类型!
注:
- 原码是十进制转二进制的概念
- 二进制第一位是符号位==>0表示“正”,1表示“负”
- 无符号数:不需要转化,也不需要符号位,原反补码相同
对于整形来说:数据在内存中存储其实存放的是补码
补码转原码的方法:
- 补码-1,符号位不变,其余按位取反
- 原码到补码过程再实现一遍
十进制二进制快速转化
口诀:1后面跟n个0,就是2的n次方
例如:int a = 100;
二进制:26+25+22
0000 0000 0000 0000 0000 0000 0110 0100 ==>100
二进制快速转十进制也可以反过来用。
你学会了吗?
大小端
大端:高字节序存储到高权值位,低字节序存储到低权值位
小端:低字节序存储到高权值位,高字节序存储到低权值位
深入理解变量内容的存入和取出
//举例
#include <stdio.h>
int main()
{
signed int a = -1;
unsigned int b = -1; //会报错吗?
//运行没有报错
return 0;
}
结论
- 存:字面数据必须先转成补码,在放入空间当中。所以,所谓符号位,完全看数据本身是否携带±号。和变量是否有符号无关!
- 取:取数据一定要先看变量本身类型,然后才决定要不要看最高符号位。如果不需要,直接二进制转成十进制。如果需要,则需要转成原码,然后才能识别。(当然,最高符号位在哪里,又要明确大小端)
如何理解signed char = -128?
signed char 在内存中的分布
-128?
从补码的意义上去理解
256-128=128
所以,256+(-128)的补码=128
所以,(-128)的补码=256-128 =128
数学上,128 = 1000 0000
故而规定:-128的补码为 1000 0000
if else—判断语句
- 什么是语句?
C语言中,由一个分号 ; 隔开的就是一条语句
例如:
printf(“hello world\n”);
1+1;- 什么是表达式?
C语言中,用各种操作符把变量连接起来,形成一个式子,叫做表达式
C语言中的操作符有+,-,*,/,%,>,<等等…
注:{}(花括号)是一个代码块
单分支结构语句
#include <stdio.h>
int main()
{
//花括号可写可不写
if(表达式)
{
语句;
}
return 0;
}
双分支结构语句
#include <stdio.h>
int main()
{
//花括号可写可不写
if(表达式)
{
语句1;
}
else
{
语句2;
}
return 0;
}
多分支结构语句
#include <stdio.h>
int main()
{
//花括号可写可不写
if(表达式)
{
语句1;
}
else if(表达式)
{
语句2;
}
else
{
语句3;
}
return 0;
}
嵌套语句
#include <stdio.h>
int main()
{
if(表达式)
{
if(表达式)
{
语句1;
}
else
{
语句2;
}
else if(表达式)
{
语句3;
}
else
{
语句4;
}
}
return 0;
}
使用方法:
- C语言中判断语句0表示假,非0表示真(负数也为真)
- if语句执行,先执行完成表达式的值,得到逻辑结果,在进行判定
- 双分支if语句为真时执行语句1,否则执行语句2
- 多分支if语句为真时执行语句1,否则判断else if语句,else if语句为真时执行语句2,为假时执行语句3
- 嵌套时如果最外层if判断为假,直接退出语句,为真时则判断第二条if语句,第二条if为真则执行语句1,否则执行语句2,后面跟之前一样…
悬空else
#include <stdio.h>
int main()
{
int a = 0;
int b = 1;
if(a == 1)
if(b==1)
printf("hello world!\n");
else
printf("Goodbye world!\n");
return 0;
}
改正:
#include <stdio.h>
int main()
{
int a = 0;
int b = 1;
if (a == 1)
{
if (b == 1)
{
printf("hello world!\n");
}
}
else
{
printf("Goodbye world!\n");
}
return 0;
}
结论:
- 第一个if没有和它匹配的else,这是写代码中经常中会犯的错误。
- 适当的使用{ }(花括号)可以使代码的逻辑更加清楚
- 代码风格很重要!
- else匹配采取“”就近原则“”
- 建议全部语句带上花括号,提高可读性
- 注意:判断表达式中等于相当于"==“,很多人写成“赋值”(=)",这里必须注意一下!
switch—开关语句
基本语法结构:
#include <stdio.h>
int main()
{
switch(整形变形/常量/表达式) //不能使用浮点数
case var1:
break;
case var2:
break;
case var3:
break;
......
case varn:
break;
default:
break;
return 0;
}
已经有if else为何还要switch case?
- switch也是一种分支语句,也常用于分支语句
- 一般指的是很多多分支,而且判定条件主要以整型为主,如:
输入一,输出星期一
输入二,输出星期二
输入三,输出星期三
输入四,输出星期四
输入五,输出星期五
输入六,输出星期六
输入七,输出星期天
如果用if else多分支语句写的话太麻烦了
#include <stdio.h>
int main()
{
int day = 0;
scanf("%d", &day);
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");
case 6:
printf("星期六\n");
break;
case 7:
printf("星期天\n");
default:
printf("输入错误\n");
break; //default不用带break也可以
}
return 0
}
case的作用是什么?
case本质是进行判定功能!
break在switch中的作用是什么?
- 在switch语句中,我们没法直接实现分支,搭配break使用才能实现真正的分支。
- break相当于直接跳出switch语句
- break语句的实际效果是把语句列表划分为不同的部分
- 多条分支不带break会一直遇到break才会停下来
#include <stdio.h>
int main()
{
int day = 0;
scanf("%d", &day);
switch(day)
{
case 1:
case 2:
case 3:
case 4:
case 5:
printf("Weekday\n");
case 6:
case 7:
printf("Weekend\n");
}
//输入1-5都是打印Weekday
//输入6-7都是打印Weekend
return ;
}
default相关问题
如果表达的值与所有的case标签的值都不匹配怎么办?
- 其实也没什么,结构就是所有的语句都被跳过而已。
程序并不会终止,也不会报错,因为这种情况在C中并不认为适合错误。
但是,如果你并不想忽略不匹配所有标签的表达式的值时该怎么办呢?- 你可以在语句列表中增加一条default子句,把下面的标签
default:
写在任何一个case标签可以出现的位置。
当 switch表达式的值并不匹配所有case标签的值时,这个default子句后面的语句就会执行。
所以,每个switch语句中只能出现一条default子句。
但是它可以出现在语句列表的任何位置,而且语句流会像贯穿一个case标签一样贯穿default子句。- 编程好习惯:在每个 switch 语句中都放一条default子句是个好习惯,甚至可以在后边再加一个 break
- default可以出现在switch内的任何部分
- 建议default放在switch语句最后面,提高可读性!
//举例
#include <stio.h>
int main()
{
int day = 0;
scanf("%d", &day);
switch(day)
{
case 1:
default:
printf("输入错误\n"); //default可以出现在switch内的任何部分
break;
case 2:
case 3:
case 4:
case 5:
printf("Weekday\n");
case 6:
case 7:
printf("Weekend\n");
}
return 0;
}
switch语句可以嵌套使用
//举例
#include <stdio.h>
int main()
{
int n = 1;
int m = 2;
switch (n)
{
case 1:
m++;
case 2:
n++;
case 3:
switch (n)
{
case 1:
n++;
case 2:
m++;
n++;
break;
}
case 4:
m++;
break;
default:
break;
}
printf("m = %d, n = %d\n", m, n);
return 0;
}
return 0;
}
运行结果:m = 5, n = 4
循环语句
1.while
语法结构
#include <stdio.h>
int main()
{
int i = 0; //条件初始化
while(条件判断) //例如i > 100
{
循环语句;
条件更新;//例如:i++或i--
}
return 0;
}
实现打印1-10的整数
#include <stdio.h>
int main()
{
int i = 1; //条件初始化
while (i <= 10) //条件判断
{
printf("%d ", i); //循环语句
i++; //条件更新
//注:i++等于i = i+1的意思
}
return 0;
}
结论:
- 我们已经学会if语句了
当条件满足的情况下,if语句后的语句执行,否则不执行。但是这个语句只会执行一次- 但是我们发现生活中很多的实际的例子是:同一件事情我们需要完成很多次!
所以c语言就引入了while这个关键字,可以依据判断重复做一件事。
while语句中的break和continue
- 在循环语句中只要遇到break,就停止后期的所有的循环,直接终止循环。 所以:while中的 break是用于永久终止循环的。如果是嵌套循环的话只能跳出(终止)内层的循环。
- continue语句在循环语句中是跳出本次循环,也就是本次循环中continue后边的代码不会再执行,而是直接跳转到while语句的判断部分。进行下一次循环的入口判断。
//break例子
#include <stdio.h>
int main()
{
int i = 1;
while (i <= 10)
{
if(i == 5)
{
break; //i == 5终止循环
}
printf("%d ", i);
i++;
}
return 0;
}
- 运行结果:1 2 3 4
- 循环执行四次后,第五次if判断为真,终止循环。
//continue例子
#include <stdio.h>
int main()
{
int i = 1;
while (i <= 10)
{
if(i == 5)
{
continue; //i == 5跳出本次循环,回到判断条件处
}
printf("%d ", i);
i++;
}
return 0;
}
- 运行结果:1 2 3 4光标一直闪烁(死循环了)
- 循环执行四次后,第五次if判断为真,跳出本次循环,回到判断条件处
因为i的值为5,所以会一直重复执行continue。
循环执行流程图:
2.do…while
语法结构:
#include <stdio.h>
int main()
{
int i = 0; //条件初始化
do
{
循环语句;
}while(条件判断); //注意:这里while括号后面带分号
return 0;
}
do语句的特点
- 循环语句至少被执行一次。
- 使用场景不多,一般用于登录客户端时输入账号密码
实现打印1-10的整数
#include <stdio.h>
int main()
{
int i = 1; //条件初始化
do
{
printf("%d ", i); //循环语句
i++; //条件更新
}while(i <= 10); //条件判断
return 0;
}
do while循环中的break和continue
break:
#include <stdio.h>
int main()
{
int i = 1;
do
{
printf("%d ", i);
if(i == 5)
{
break; //终止(跳出)全部循环
}
i++;
}
return 0;
}while(i <= 10);
- 运行结果:1 2 3 4 5
- 循环执行到第五次,打印完i后,判断if为真,终止循环
注:while括号后面加分号;
continue:
#include <stdio.h>
#include <windows.h>
int main()
{
int i = 0;
do
{
Sleep(1000); //睡眠函数,停留一秒
printf("%d ", i);
if(i == 5)
{
continue;
}
i++;
}while(i <=10);
return 0;
}
- 运行结果:1 2 3 4 5 5 5 5…死循环
- 循环第五次中,if判断为真,跳出本次循环,回到判断条件处
因为i的值为5,所以会一直重复打印5并且重复执行continue
注:while括号后面加分号;
do while语句执行流程图
3.for
for语法结构
#include <stdio.h>
int main()
{
int i = 0;
for(条件初始化(表达式1);条件判断(表达式2);条件更新(表达式3))
{
循环语句;
}
return 0;
}
- 表达式1 表达式1为初始化部分,用于初始化循环变量的
- 表达式2 表达式2为条件判断部分,用于判断循环时候终止。
- 表达式3 表达式3为调整部分,用于循环条件的调整。
实现打印1-10的整数
#include <stdio.h>
int main()
{
int i = 0; //定义变量
for(i = 1; i <= 10; i++)
{
printf("%d ", i);
}
return 0;
}
注:for语句是先执行表达式1和表达式2后执行循环语句,最后执行条件更新
for语句中的break和continue
break:
#include <stdio.h>
int main()
{
int i = 0;
for(i = 0; i < 10; i++)
{
if(i == 5)
{
break;
}
printf("%d ", i);
}
return 0;
}
- 运行结果:1 2 3 4
- 循环执行到第五次,判断if为真,终止循环
continue:
#include <stdio.h>
int main()
{
int i = 0;
for(i = 1; i <= 10; i++)
{
if(i == 5)
{
continue;
}
printf("%d ", i);
}
return 0;
}
- 运行结果:1 2 3 4 6 7 8 9 10
- 循环第五次中,if判断为真,跳出本次循环
回到条件更新处,i++后为6,然后进入判断
for语句循环的变量控制
- 不可在for 循环体内修改循环变量,防止 for 循环失去控制。
- 建议for语句的循环控制变量的取值采用“前闭后开区间”写法。
#include <stdio.h>
int main()
{
int i = 0;
//前闭后开区间,可读性高
for(i = 0; i < 10; i++) //循环十次
{
;
}
//前闭后闭区间,可读性差
for(i = 1; i <= 9; i++) //循环十次
{
;
}
return 0;
}
for语句执行流程图
goto语句
语法结构
#include <stdio.h>
int main()
{
goto 标识跳转符;
标识跳转符:
return 0;
}
使用goto模拟实现循环
#include <stdio.h>
int main()
{
int i = 0;
START:
printf("[%d]goto running ... \n", i);
++i;
if (i < 10) {
goto START;
}
printf("goto end ... \n");
return 0;
}
使用goto语句实现跳出多层循环
使用goto语句跳出嵌套循环(举例跳出第十次嵌套循环)
#include <stdio.h>
int main()
{
int i = 0;
int j = 0;
for(i = 1; i <= 10; i++)
{
for(j = 1; j < i+1; j++)
{
if(i == 10)
{
goto END; //i等于10时,跳出嵌套循环
}
printf("%d*%d=%d ", i, j, i * j);
}
printf("\n");
}
END: //跳到标识符这里
return 0;
}
建议:
- 很多公司确实禁止使用goto关键字,不过呢,这个问题我们要灵活的去看待,不要太古板,goto关键字在解决适合的场景是有奇效的。
- 使用goto关键字时,要深刻的理解他的语法,防止跳过不必要的语句,造成错误。
- 一个工程(程序)里面禁止频繁使用goto关键字,防止可读性差。
Linux内核中充满了大量的goto语句,只能说我们目前,写的程序逻辑并不是特么的复杂。
void—空类型
为什么void不能定义变量?
#include <stdio.h>
int main()
{
void a = 0; //报错
return 0;
}
- 定义变量的本质:在内存中开辟特定大小的空间,大小看类型
- void是空类型,就是空的意思,不能在内存中开辟空间,是一个占位符
- 既然无法开辟空间,那么也就无法作为正常变量使用,既然无法使用,编译器干脆不让他定义变量
- 在vs2019中sizeof(void)运行结果是0
- 在Linux环境下sizeof(void)运行结果是1(但编译器依旧理解成,无法定义变量)
void修饰函数返回值和参数
void作为函数返回值时
//举例
#include <stdio.h>
void test()
{
printf("hello world\n");
}
int main()
{
test();
return 0;
}
- 自定义函数没有返回值,可以把函数返回值写成void
- 自定义函数没有写返回值时,会默认返回值为int
- 如果没有写返回值,会导致可读性变差
- void作为函数返回值,代表不需要,这里是一个"占位符"的概念,是告知编译器和给阅读源代码的工程师看的
void作为函数参数时
//举例
#include <stdio.h>
int test1() 函数默认不需要参数
{
return 1;
}
int test2(void) 明确函数不需要参数
{
return 2;
}
int main()
{
printf("test1:%d\n", test1(100)); //依旧传入参数,没有报错
printf("test2:%d\n", test2(200)); //依旧传入参数,有警告,但可以编译
return 0;
}
结论:
- 如果一个函数没有参数,建议将函数形参设置成void,是一个不错的习惯,因为可以将错误明确提前发现
- 另外,阅读你代码的人,也一眼看出,不需要参数。相当于"自解释"
void指针
void*可以定义变量吗?
//举例
#include <stdio.h>
int main()
{
void* p = NULL; //没有报错
return 0;
}
- void*是一个空类型指针,因为它是一个指针,就有空间的大小
- 指针在内存中开辟的空间是4(32位)或8(64位)个字节
void 能够接受任意指针类型*
//举例
#include <stdio.h>
int main()
{
void *p = NULL;
int *x = NULL;
double *y = NULL;
p = x; //虽然类型不同,但是编译器并不报错
p = y; //同上
return 0;
}
结论:
- void*的作用是用来接受任意指针类型的
- void*可以用来设计函数的通用接口
- 比如:void * memset ( void * ptr, int value, size_t num );
void * 定义的指针变量可以进行运算操作吗
//举例
#include <stdio.h>
int main()
{
void *p = NULL;
p++; //报错
p += 1; //报错
return 0;
}char
结论:
- 空类型指针虽然可以在内存中开辟空间,但是指针的类型是void,void代表什么都没有的意思,也就是说在进行自增运算时,地址移动步数为0
- 注:指针变量进行运算时,是看类型来决定移动步数的
return(返回值)
分析以下这段代码
#include <stdio.h>
char* test()
{
char arr[] = "hello world"; //定义具有临时性的数组变量
return arr; //返回数组首元素地址(char*)
}
int main()
{
char* str = test(); //调用函数,返回类型是char*
printf("&s\n", str);
return 0;
}
运行结果:
分析:
- 在主函数定义了字符指针变量,调用函数,形成函数栈帧,函数体内定义了字符类型数组,返回数组首元素地址,函数栈帧被释放。
- 我们可以在调试中看到,被释放后,主函数里面的字符指针还是指向了test函数,变量里面的内容没有发生任何改变。
执行格式化输出函数后字符指针变量所指向的内容已被printf函数所覆盖。
- 调用函数,形成函数栈帧
- 函数返回,函数栈帧释放
- 注:函数体内定义的变量具有临时性
返回值临时变量接收的本质
#include <stdio.h>
int test()
{
int a = 10;
return a;
//既然a是函数定义的变量,具有临时性,那么这个临时变量在函数退出的时候,应该被释放
}
int main()
{
int n = test();
printf("%d\n", n);
return 0;
}
结论:
- 通过查看反汇编可以看到,对于一般内置类型,寄存器eax可以充当返回值的临时空间
如果没有变量接收返回值,有返回值吗?
#include <stdio.h>
int test()
{
int a = 10;
return a;
}
int main()
{
test();
return 0;
}
结论:
- 没有返回值,因为没有变量接收
- 返回值被存储在寄存器中,主函数没有变量接收,就意味着没有返回值
const—常变量
const修饰变量
//举例
#include <stdio.h>
int main()
{
const int n = 10;
n = 20; //报错,不能直接被修改
printf("%d\n", n);
return 0;
}
分析:
- const修饰的变量,不会真正意义上变成常量,其实还是一个变量。
我们可以叫他"常变量"- 被const修饰后的变量,不能进行二次赋值(右值概念)
const修饰变量真的不能被修改吗?
//举例
#include <stdio.h>
int main()
{
const int n = 10;
int* pn = (int*)&n;
*pn = 20;
printf("%d\n", *pn);
return 0;
//运行结果:20
}
结论:
- 可以通过指针的方式间接改变n的值。
- 那const修饰变量,意义何在?
1.让编译器进行直接修改式检查
2.告诉其他程序员(正在改你代码或者阅读你代码的)这个变量后面不要改,也属于一种“自描述”含义
const修饰的变量,可以作为数组定义的一部分吗?
//举例
#include <stdio.h>
int main()
{
const int n = 10;
int arr[n]; //报错
return 0;
}
报错原因:
- 上面已经说过了,被const修饰后还是变量,只是不能进行二次赋值
- 数组方括号里面只能是常量,而n是一个变量(c99之前标准)
- c99标准支持变长数组,数组方括号里面可以是变量
const修饰数组
定义或说明一个可读数组可以如下定义:
//举例
#include <stdio.h>
int main()
{
const int arr[] = {0, 1, 2, 3};
int const arr[] = {0, 1, 2, 3};
return 0;
}
const修饰指针
//举例
#include <stdio.h>
int main()
{
int n =10;
const int* p = NULL;
p = &n;
*p = 20; //报错
printf("%d\n", *p);
return 0;
}
结论:
- 在星号前面修饰指针变量时,p变量可以被赋值,而*p(指针所指向的目标==n)不能进行二次赋值!
- 影响的是指针解引用时的右值概念(赋值)
- 注:int const *p也是一样的!
//举例
#include <stdio.h>
int main()
{
int n = 10;
int* const p = NULL;
p = &n;
*p = 20;
printf("%d\n", *p);
return 0;
}
结论:
- 在星号后面修饰指针变量时,p变量(指针变量)不可以进行二次赋值,而*p(指针所指向的目标==n)能进行二次赋值!
- 影响的是指针变量的右值概念(赋值)
//举例
#include <stdio.h>
int main()
{
int n = 10;
const int* const p = NULL;
p = &n;
*p = 20;
printf("%d\n", *p);
return 0;
}
结论:
- 在星号前面后面都修饰指针变量时,p变量(指针变量)和*p(指针所指向的目标==n)都不能能进行二次赋值!
- 影响的是指针变量的右值概念(赋值)
const修饰函数形式参数
//举例
#include <stdio.h>
void test(const int* p)
{
}
int main()
{
int n = 10;
test(&n);
return 0;
}
分析:
- const修饰函数形式参数时,是告诉我们在函数体内不能更改参数的值
- 告诉编译器*p在函数体不能被改变,从而防止使用者的无意或错误修改,也相当于一种自描述。
const修饰函数返回值
例如:const int test();
- 被const修饰后,函数的返回值不可以被改变
#include <stdio.h>
const int* test()
{
static int g_var = 100;
return &g_var;
}
int main()
{
int* p = test(); //有告警
//const int *p = test(); //需要用const int*类型接受
*p = 200; //这样,在语法上,也就限制了,不能直接修改函数的返回值
printf("%d\n", *p);
return 0;
}
最易变的关键字—volatile
- 本质:volatile是易变,不稳定的意思。很多人学习c语言从来没有遇见过这个关键字,也压根也用不上。
- 对他有种“杨家有女初长成,养在深闺未人识”的感觉
- volatile跟const关键字一样,是用来修饰变量的
未加volatile修饰全局变量
//举例
#include <stdio.h>
int pass = 1;
int main()
{
while(pass)
{
}
return 0;
}
查看汇编代码
加volatile修饰全局变量
//举例
#include <stdio.h>
volatile int pass = 1;
int main()
{
while (pass)
{
}
return 0;
}
汇编代码
结论:
- volatile忽略编译器的优化,保持内存可见性。
- 防止变量被存储到寄存器,导致变量的内存不能进行访问和读写
其他问题
constvolatileinta=10;
- const是在编译期间起效果
- volatile在编译期间主要影响编译器,形成不优化的代码,进而影响运行,故:编译和运行都起效果
- const要求你不要进行写入就可以。
- volatile意思是你读取的时候,每次都要从内存读。两者并不冲突
- 虽然volatile就叫做易变关键字,但这里仅仅是描述它修饰的变量可能会变化,要编译器注意,并不是它要求对应变量必须变化!这点要特别注意
struct—结构体
结构体是什么?
- 结构体是将多种类型封装成一个整体的关键字
- 结构的成员可以是变量、数组、指针,甚至是其他结构体等等…
注:c语言没有字符串类型,但是有字符串
结构体基本语法结构
#include <stdio.h>
struct 结构体名字 //结构体类型
{
各种类型;
.....
}结构体变量名; //结构体的标识符
int main()
{
//使用
struct 结构体名字 结构体变量名 = {对应类型赋值逗号隔开}; //结构体初始化
return 0;
}
结构体成员的访问
//举例:描述一本书
#include <stdio.h>
struct Book
{
char name[20]; //书名
char author[10]; //作者
float price; //价格
}s;
int main()
{
//赋值
struct Book s = {"阿里嘎多", "自己", 9.9f};
//打印
printf("%s %s %.1f\n", s.name, s.author, s.price);
//运行结果:阿里嘎多 自己 9.9
return 0;
}
注:打印结构体里面的变量时,使用".(点)"操作符来进行访问
使用结构体指针访问其内部成员
#include <stdio.h>
struct Book
{
char name[20]; //书名
char author[10]; //作者
float price; //价格
}s,*p;
int main()
{
//赋值
struct Book s = { "阿里嘎多", "自己", 9.9f };
p = &s;
//打印
printf("%s %s %.1f\n", p->name, p->author, p->price);
//运行结果:阿里嘎多 自己 9.9
return 0;
}
注:打印结构体里面的变量时可以用*( p).或p->来访问结构体里面的变量
结构体传参
//举例
#include <stdio.h>
struct S
{
int data[10];
int num;
};
struct S s = {{1,2,3,4}, 10};
//结构体传参
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;
}
上面传值还是传址好?
- 当然是传址好,形参指针接收,只占4/8个字节
- 函数传参的时候,参数是需要压栈的。
- 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的
下降
结构体在内存中的对齐规则
- 结构体中元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。从结构体存储的首地址开始,每一个元素放置到内存中时,它都会认为内存是以它自己的大小来划分的,因此元素放置的位置一定会在自己宽度的整数倍上开始(以结构体变量首地址为0计算)。
- 在经过第一原则分析后,检查计算出的存储单元是否为所有元素中最宽的元素的长度的整数倍,是,则结束;若不是,则补齐为它的整数倍。
#include <stdio.h>
struct Test
{
char a;
int b;
double c;
}s;
int main()
{
//举例
printf("%d\n", sizeof(s));
}
分析:
- 对其规则1
- 结构体存储变量是从0开始的,一开始0存储字符变量,然后存储整形变量发现第一块四个字节空间变量里面有值(0,1,2,3),因此只能存入第二块四个字节空间里面去(4,5,6,7),接下来存储double类型时,会找到8的倍数位空间去存储(8-15),空间里最大偏移量是double类型为8,则结构体所占的字节个数可以被8整除才行,所以是16,16是8的倍数
#include <stdio.h>
struct Test
{
char a;
double b;
int c;
}s;
int main()
{
//举例
printf("%d\n", sizeof(s));
}
分析:
- 对齐规则2
为什么存在内存对齐?
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特
定类型的数据,否则抛出硬件异常。- 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访
问。- 结论:结构体的内存对齐是拿空间来换取时间的做法。
所以说,我们要做到让占用空间小的成员尽量集中在一起。
默认对齐数
- 使用#pragma这个预处理指令可以设置默认对齐数
- 例如:#pragma pack(8) ==>默认对齐数设置成了8
#include <stdio.h>
#pragma pack(8) //设置默认对齐数为8
struct stu
{
char a;
int b;
char c;
}s;
#pragma pack() //取消默认对齐数
#pragma pack(1) //设置默认对齐数为1
struct stu
{
char a1;
int b1;
char c1;
}s1;
#pragma pack() //取消默认对齐数
int main()
{
printf("%d\n", sizeof(s));
printf("%d\n", sizeof(s1));
return 0;
}
结论:
- 结构在对齐方式不合适的时候,我们可以自己修改默认对齐数。
enum—枚举
枚举顾名思义就是一一列举。
把可能的取值一一列举。
例如:
- 颜色中有红色,蓝色,黑色等等…
- 人的性别有男,女,保密…
枚举的使用
#include <stdio.h>
//枚举的定义
enum Colour //定义
{
RED, //红色
BLUE, //蓝色
BLACK //黑色
};
int main()
{
enum Colour s = RED; //初始化
enum Colour s = BLUE;
enum Colour s = BLACK;
return 0;
}
- 以上定义的enum Colour是枚举类型。
- {}中的内容是枚举类型的可能取值,也叫 枚举常量 。
- 这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
#include <stdio.h>
enum Colour
{
RED = 1,
BLUE = 3,
BLACK = 2
};
int main()
{
return 0;
}
枚举的优点
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
union—联合体(共用体)
联合体——顾名思义,多个变量共用一个空间
把定义的变量都放在一个空间里面存储
#include <stdio.h>
union Un //声明
{
char a;
int b;
};
int main()
{
union Un n;//定义
printf("%d\n", sizeof(n)); //在内存中所占的空间大小
return 0;
}
特点:联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合体至少得有能力保存最大的那个成员)。
联合体的使用
#include <stdio.h>
union Un
{
char a;
int b;
}n;
int main()
{
//访问联合体内部变量
n.a = 'a';
n.b = 0;
printf("%c %d\n", n.a, n.b);
return 0;
}
联合体的大小计算
- 联合的大小至少是最大成员的大小。
- 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
#include <stdio.h>
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
int main()
{
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
return 0;
}
typedef—重定义类型
本质:
- 对一般类型进行重命名
- 对结构体类型进行重命名
- 对指针进行重命名
- 对复杂结构进行重命名
typedef和#define的区别
//举例
#include <stdio.h>
#define TEST int*
typedef int* int_t;
int main()
{
//二者的区别
TEST a1,b1;
int_t a1,b1;
return 0;
}
结论:
- #define在预定义的时候只是简单的进行了文本替换而已==>int* a,b;
- typedef在重定义一个类型时会生成一个全新的类型==>int_t a,b;
第二个问题
#include <stdio.h>
#define INT32 int
typedef int int32;
int main()
{
unsigned int32 a = 10; //C中typedef不支持这种类型的扩展,不能当成简单的宏替换
//unsigned INT32 a = 20; //宏简单替换,可以
//printf("%d\n", sizeof(struct stu));
return 0;
}
结论:
- typedef重定义的是一个全新的类型,而类型在定义时不能存在二个
- #define是简单的进行了文本替换,他不是一个新的类型
typedef static int int32_t 行不行?
不行,存储关键字,不可以同时出现,也就是说,在一个变量定义的时候,只能有一个
关键字分类
数据类型关键字(12个)
- char :声明字符型变量或函数
short :声明短整型变量或函数
int : 声明整型变量或函数
long :声明长整型变量或函数
signed :声明有符号类型变量或函数
unsigned :声明无符号类型变量或函数
float :声明浮点型变量或函数
double :声明双精度变量或函数
struct :声明结构体变量或函数
union :声明共用体(联合)数据类型
enum :声明枚举类型
void :声明函数无返回值或无参数,声明无类型指针
控制语句关键字(12个)
循环控制(5个)
- for :一种循环语句
do :循环语句的循环体
while :循环语句的循环条件
break :跳出当前循环
continue :结束当前循环,开始下一轮循环
条件语句(3个)
- if : 条件语句
else :条件语句否定分支
goto :无条件跳转语句
开关语句 (3个)
- switch :用于开关语句
case :开关语句分支
default :开关语句中的“其他”分支
返回语句(1个)
- return :函数返回语句(可以带参数,也看不带参数)
存储类型关键字(5个)
- auto :声明自动变量,一般不使用
extern :声明变量是在其他文件中声明
register :声明寄存器变量
static :声明静态变量
typedef :用以给数据类型取别名(但是该关键字被分到存储关键字分类中,虽然看起来没什么相关性)
其他关键字(3个)
- const :声明只读变量
sizeof :计算数据类型长度
volatile :说明变量在程序执行中可被隐含地改变
关键字内容已经全部更新完,感谢大家的支持,有不对的地方请大家多多指点,感谢!!!