你真的对各个关键字熟悉了吗?详细再介绍,基础再提高(小白友好)下

C语言关键字2

分支语句:if

分支语句:if,先执行 ( ) 中的表达式或者是函数,得到结果为真,则执行函数体,为假则不执行函数体。

在编码原则中,为了使代码执行清晰,会选择将执行概率更大的代码放在 if 判断中,而将执行概率小的放在 else 中,就是说,要将正常情况放在放在 if 后面进行处理,将异常的 / 意外的情况 放在 else中进行判断执行。

还有在使用 if-else if 结构时,最后应该以 else 结尾,最后的else 语句要 执行适当的动作,要包含合适的注释以说明为何没有执行动作,与switch 语句最后要求具有一个 default 子句是一致的。

C语言中的 bool 类型

在 C99 标准之前,也就是C89 或者说是C90 中,认为是没有 bool 类型的,而在C99 中引入了 _Bool 类型,(就是 _Bool 类型,只是在新增的头文件 stdbool.h 中,使用宏定义写成了 bool ,为了保证C和C++ 的兼容性。

在使用微软的编辑器中(VS系列或VSCode)中,大写的 BOOL 类型和 TRUE / FALSE 都是可以支持的,但是这并不是C99 的标准,所以推荐些小写 : bool 和 true / false

对于bool型的使用,要注意以下几点:

#include <string.h>
#include <stdbool.h>
int main()
{
    int flag = 0;
	if (flag == 0)
	{
		//不推荐此写法
		//一般用于 int 型的比较,容易造成代码可读性降低
		printf("1\n");
	}
	
	if (flag==false)
	{
		//不推荐次写法
		//false 仅在C99 及以上支持,代码可移植性不够
		printf("2\n");
	}

	if (!flag)
	{
		//推荐,可以直观表示bool类型
		printf("3\n");
	}
    
    
    return 0;
}
	

总之,bool 类型直接作为 判定 条件,不需要把操作符和特定值进行比较

浮点数问题

先来看一个简单的问题:


double x = 1.0;
double y = 0.1;
printf("%.50f\n", x - 0.9);
printf("%.50f\n", y);

if ((x - 0.9) == 0.1)
{
	printf("I can hear you!!");
}
else
{
	printf("Oh NO !!!");

}
/*
运行结果:
0.09999999999999997779553950749686919152736663818359
0.10000000000000000555111512312578270211815834045410
Oh NO !!!
*/

从这个例子中,就要明白,在浮点数进行比较的时候,绝对不能使用 == 来进行比较!!! 浮点数本身就会有精度损失,所以会导致各种结果会有细微差别

所以,如果要比较浮点数的大小,就可通过 做差 与精度进行比较的方法来确定大小

就可以采用如下的精度比较的办法:

#include "test.h"
#include <string.h>
#include <stdbool.h>
#include <math.h>
#define EPS 0.00000001
 int main()
{
    double x = 1.0;
    double y = 0.1;
    printf("%.50f\n", x - 0.9);
    printf("%.50f\n", y);

    if(fabs((x-0.9)-y)<EPS)
    {
        printf("I can hear you!!");
    }
    else
    {
        printf("Oh NO !!!");

    }
     
     return 0;
}


/*
运行结果:
0.09999999999999997779553950749686919152736663818359
0.10000000000000000555111512312578270211815834045410
I can hear you!!
*/


在浮点数的比较中,如果不使用自己宏定义的常量,也可以使用系统自定义的一个常量来作为比较对象

#include<float.h> //需要包含的头文件
DBL_EPSILON		//double 最下精度值
FLE_EPSILON		//float 最小精度值

如下:

#include <stdio.h>
#include <float.h>

int main()
{
    double x = 0.000000001;
if (fabs(x)<DBL_EPSILON)
{
	printf("I can hear you!!");
}
else
{
	printf("Oh No!!!");
}
    return 0;
}
//运行结果:
//Oh No!!!

如果足够小的话,就可以得到想要的结果了:

#include <stdio.h>
#include <float.h>

int main()
{
    double x = 0.0000000000000001;
if (fabs(x)<DBL_EPSILON)
{
	printf("I can hear you!!");
}
else
{
	printf("Oh No!!!");
}
    return 0;
} 
//运行结果:
//I can hear you!!

在进行比较时,尽量不要写等于号,因为即使 xxx_EPSILON 这个数很小, 也不能认为它 就是0,所以在使用它时,是不可以和 0 等价的

浮点数总结:

  1. 浮点数存储 是有精度损失的
  2. 浮点数不能进行 == 的比较
  3. 使用 if ( fabs ( a - b ) < DEL_EPSILON ) { } 来进行浮点数比较
  4. 不需要使用 <= 或者 >= 等于号是没有必要的

类型转换的关系

强制类型转换:不改变内存中的数据(二进制数是不变的),只改变对应的类型

数据转换:改变内存中的数据,比如字符串 “12345” 转换为 int 型的 12345,是由6个字节转换为了四字节,内存中的二进制数发生了变化。

指针变量与 “零值” 的比较

指针 就是地址

指针变量 就是一个变量,里面存放的内容是地址

对于空指针的判断,要注意规范

int *p = NULL;
if(p == 0)  	if(p != 0 )  		//不推荐
if ( p )  		if( !p )  			//不推荐
if( NULL == p )	if( NULL != p) 		//推荐写法

选择语句 switch-case的使用

case 作为条件判断的依据,break 则作为分支功能的存在

有以下注意点:

  • 每个case语句的结尾不要忘记了 break,否则将导致多个分支重叠(除非有意使多个分支重叠)
  • 在所有case 语句之后,一定要加上 default 语句,表示所有case都不符合的情形
  • 习惯上将 default 语句放在最后,即使将default 语句放在switch 语句的任何位置都可以,但是习惯如此,而且,在实际使用中,default语句最好是用于默认的其他情形,而不要是预想的最后一种情形。
  • 匹配一个条件执行多个语句时,要注意在一个条件中是无法定义变量的,如果一定要定义新变量,则需要使用 { } 来进行包装,使之成为一个代码块。实际编程中,并不推荐这个写法,最好将代码块封装为函数
  • 在case语句的匹配中,case后面的必须为真正的 int、char 等真正的常量,而不应该是被const 修饰的(虚假的)常量,这样的常量是无法通过编译的,但是使用 #define 定义的常量是可以通过编译的
  • 一般在正常情况下需要按照一定的顺序将case 语句进行排列,遇到特殊的情形,也应该把最频繁执行的case 语句放在前面,执行次数较少的 case 语句放在后面
  • 多个case 语句可以使用多条件匹配:
    10

C程序的运行

任何C程序,在默认编译好之后,在运行时,都会打开三个输入输出流: 可以理解为一切皆为文件

  • stdin : 标准输入,FILE* stdin; --> 键盘
  • stdout : 标准输出,FILE* stdout; --> 显示器
  • stdrr : 错误输出,FILE* stderr; --> 显示器

所以可以将 键盘、显示器 称为 字符型设备

这样就需要正确理解 getchar 的使用,使用getcahr 进行接收字符时,先是接受各个字符,然后格式化为指定的类型,放入变量中,所以要注意该函数会默认读入输入的最后一个回车符,同样的,在进行输出时,显示在屏幕上的也全部都是字符,int 型也是以字符 格式化 并显示在屏幕上的,例子:

//在屏幕上输出的内容就是字符,并且每个都对应一个ASCII字符
int ret = printf("%d\n", 1234);
printf("%d\n", ret);	//四个字符和一个换行符,就是5了
/*
运行结果:
1234
5
*/

break终止本层循环

continue终止本次循环

循环语句的使用

  • 在多重循环中,如果有可能,应将最长的循环放在最内层,最短的循环放在最外层,可以减少CPU 跨切循环的次数(不强求)

  • 使用for 循环控制变量的取值 采用 “半开半闭区间”写法(可以直观的表示出循环次数)
    11

  • 循环体要尽可能的简介,循环嵌套尽量不要超过三层

  • goto 语句尽量不要使用,虽然可以灵活跳转(在代码块内,无法跨函数,跨文件)但是如果不加限制,就会破坏结构化设计风格,甚至有可能会带来错误或隐患,会跳过变量的初始化、重要的计算等语句。

对返回值的理解

先看代码:

void test()
{
	printf("hello world!");
	return 1;
}

int main()
{
	test();
    return 0;
}

这样的代码是不会报错且可以正常运行的,运行的结果就是: hello world!虽说函数是void 类型,但是却强加了一个return 语句,因为在运行的时候,是直接运行了 test() 函数,并没有明确规定接受该函数的结果的类型,如果做了明确规定,比如 int a = test(); 那么就会直接报错了。

在C语言中也要注意,void是被解释为无大小的类型,但是他本身也是有大小,如果使用VS 来 输出 sizeof (void ) 会得到结果为 0 ,而如果使用gcc(Linux) 来输出 sizeof(void ) 则会得到结果为1,要切实注意。

C语言中函数可以不使用返回值,函数的默认的返回值为 int ,但是不推荐使用这种写法,要明确函数的返回值

void 修饰函数的返回值,可以作为占位符:让用户明确知道不需要返回值,也可以告知编译器,这个无法接受返回值

void 充当函数的形参列表,告知用户/编译器,该函数不需要参数

但是 void* 可以用来接受任意指针类型,这样就可以用来设计通用接口

为什么VS中和Linux中C语言有所不同?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mxvZOjSM-1633229320859)(D:\OneDrive - iswilliam\博客学习\C语言关键字\12.png)]

函数调用的内存分配

在 C语言中,在调用一个函数时,就是在内存的 栈区 开辟一段空间,这段空间用来存放该函数的所有内容,不会太大占用过多内存空间,也不会过小,导致无法成功放入函数,这样的可以正好放下一个函数的内存空间就可以成为 栈帧。

调用函数,形成栈帧, 函数返回,销毁栈帧(数据依然在内容中,只是指向该栈帧的指针移动了位置)

该栈帧的大小是由 程序在编译时,对各个变量类型的大小的预估得到的,所以大小是正好合适的。

这样也可以解释:为什么 临时变量是具有临时性的?

因为栈帧结构在函数调用完毕后,需要被释放

下面个例子可以更好的理解临时性:

#include <stdio.h>
#include <string.h>

int GetData()
{
	int x = 0x11223344;
	printf("run get data!\n");
	return x;
}
int main()
{
	int y = GetData();
	printf("ret: %x\n", y);

/*
运行结果:
run get data!
ret: 11223344
*/

	return 0;
}

只看结果的话,在函数调用时,成功的输出了函数中的临时变量的内容,而不是取到了 调用函数中的变量

通过反汇编的方式来观察地址跳转,可以发现: 函数的返回值,通过寄存器的方式返回给函数调用方,而且无论是否有变量接受函数返回值,函数都会将返回值放入寄存器中

const关键字的认识

const 修饰的变量不可以被直接修改,但是可以通过修改指针来实现修改:

	const int a = 10;	//a 不可以被直接修改
	int* p = (int*)&a;	//转换为 指针类型
	printf("before: %d\n", a);

	*p = 20;	//解引用

	printf("after: %d\n", a);
/*
before: 10
after: 20
*/

所以使用const 进行修饰的意义是:

  • 告知编译器进行 直接修改式 检查,防止被修改,可以防止可能出现的逻辑问题
  • 告知其他程序员,此变量后续 不要进行修改,有"自描述"含义

真正的不可修改:
image

所以说使用const 修饰的变量并不真的是变成了常量,仅仅是作为不可轻易修改的变量:
image

此段代码在 VS 编译器下无法正常运行,在 gcc (GNU 拓展) 下就可以正常运行了,也就是在Linux 平台可以正常编译运行,所以处于跨平台性,不要出现这种写法

如何取地址

在C语言中,任何变量在取地址时,都是从最低地址开始的

理解 常量指针 和 指针常量

常量指针:

int a = 10;
int b = 20;
//常量指针
const int* p = &a;	//p指向的变量不可以直接被修改,保存的地址可以被修改
//int const* p = &a;	//表达含义与上相同,一般使用上面的定义方法,比较规范
//*p = &b;	//无法实现,因为 *p 被const 修饰了,其保存的地址无法被修改
p = 100;	//可以实现,因为 p 没有被const 修饰,可以正常被修改

指针常量:

int a = 10;
int b = 20;

//指针常量
int* const p = &a;	//p 保存的地址不可直接被修改, 但是该地址的内容 可以被修改
*p = 100;	//可以实现,因为 *p 相当于a,可以修改内容
//p = 100;	//无法实现,因为 p 被const 修饰,无法被修改

const 在函数中的使用

const 修饰函数参数,可以保证再函数内部不会对参数进行修改,一般输出函数会经常使用 const 来修饰形参,防止被修改

const 修饰函数(返回值),可以保证不能 通过指针修改返回值指向的内容

#include <stdbool.h>

const int* GetVal()
{
	static int a = 10;
	return &a;
}

int main()
{
	//要使用同样类型的变量来接收 函数返回值
	const int* p = GetVal();
	//无法实现通过指针来修改返回值了
	// *p = 100;	//会报错
	
	
	return 0;
}

最易变的关键字:volatile

面试时仅次于const被问到的概率

在现在的编程时,这个关键字是很没有存在感的,在后续进行高并发编程、网络编程时可能会有所应用。

volatile 的意思是 易变的,不稳定的,它和 const 一样是 一中类型修饰符,用它修饰的变量 表示可以被某些编译器位置的因素更改,比如 操作系统、硬件、或者其他线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

使用此关机字可以达到不被编译器(CPU)优化,从而可以稳定访问内存的作用

#include <stdio.h>
int pass = 1;
int main()
{
    while(pass)
    { 
        //思考一下,这个代码有哪些地方,编译器是可以优化的。
    } 
    return 0;
}

使用Linux 下 gcc 进行优化:
image

如果使用volatile 关键字,就可以如下得到:

#include <stdio.h>
volatile int pass = 1; //加上volatile
int main()
{
    while(pass)
    {
        
    } 
    return 0;
}

不使用优化:

 gcc test.c -O2 -g //以O2级别进行代码优化
 objdump -S -d a.out > aa.s //对形成的a.out可执行程序进行优化
 vim aa.s //查看汇编代码

image

const 和 volatile 并不冲突

const volatile int a = 10;

在vs2013和gcc 4.8中都能编译通过
const是在编译期间起效果
volatile在编译期间主要影响编译器,形成不优化的代码,进而影响运行,故:编译和运行都起效果。

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

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

最会帽子的关键字:extern

在声明变量时用来 告知 这是一个声明,而不是未初始化的定义

结构体关键字:struct

结构体:具有不同类型数据结构的集合

结构体可以初始化,但是不可以整体赋值,可以实现对结构体内的单个成员变量的赋值操作。

如果 空结构体,使用VS 是无法编译通过的,而使用 Linux下的 gcc 编译器就可以正常的编译通过,并且可以得到空结构体的大小为零,也就是成功开辟了大小为零的地址空间,用来存放结构体,这是因为 void 是作为 特殊关键字 使用的,所以在编译阶段,编译器就不予许为空,就无法通过编译,而且即使有编译器可以使其通过编译,void 的变量由于没有大小空间,也无法正常使用;而struct 是如果为空,则没有任何内容,可以通过编译。

柔性数组

在C99 标准中,结构中的最后一个元素允许是未知大小的数组,即为柔性数组,但结构中的柔性数组成员前面必须至少有一个其他成员。

sizeof 返回的这种结构大小不包括柔性数组的内容,包好柔性数组成员的结构用malloc() 函数进行内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

#include<iostream>
using namespace std;

struct stu
{
    //使用一个其他成员
    char name[20];
    //柔性数组,即使将 0 去掉,也可以正常的编译
	int arr[0];
};

int main()
{
    //可以正常编译通过,且柔性数组不占用结构体大小空间,此时结构体大小为 4b
	struct stu zhangsan;
	return 0;
}

柔性数组的大小:image

联合关键字 union

在 union 中所有数据成员公用一个空间,同一时间只能存储其中一个数据成员,所有的数据成员具有相同的起始地址

一个 union 只配置一个足够大的空间来容纳最大长度的数据成员,在C++ 中union的成员默认属性为 public,主要用来压缩空间,如果一些数据不可能再同一时间同时被用到,就可以使用union

联合体的地址和联合体变量的地址为同一个:因为整个联合体的地址空间都是由最大的变量开辟的,所以起始地址也是一样的了,而较小的变量的地址则是从低地址开始开辟的

#include<stdio.h>
union un
{
	int a;
	char b;
};

int main()
{
	union un x;
	printf("un 的大小为:%d\n", sizeof(union un));
	printf("un 的地址为:%p\n", &x);
	printf("un中a的地址为:%p\n", &(x.a));
    printf("un中b的地址为:%p\n", &(x.b));
/*
运行结果:
un 的大小为:4
un 的地址为:00B3F838
un中a的地址为:00B3F838
un中b的地址为:00B3F838
*/

	return 0;
}

联合体内所有变量的其实地址都是一样的(都是从低地址开始存放),每个联合体内的变量都可以认为是第一个元素

而且在数据存储时,采用的是小端存储,就是数据的低权值位是存储在联合体的低地址位 的。

还有一点,在判断联合体占用空间大小时要考虑内存对齐!

#include<stdio.h>
union un
{
	int a;
	char b[5];
    //在考虑内存对齐整除时,将这个数组看为char,为1,但是在计算大小时看做整体,为5
};

int main()
{
	printf("un 的大小为:%d\n", sizeof(union un));

	return 0;
}
/*
运行结果为:
un 的大小为:8
*/

联合体内存对齐:联合体所占的内存大小,需要可以整除联合体内任何一个变量的大小

枚举关键字:enum

枚举关键字定义的均为常量,枚举变量可以直接作为常量使用,甚至可以将枚举常量看做是整数

如果对枚举常量进行赋值,则其后是连续赋值的,也可以实现阶段性赋值

#include<stdio.h>

enum color
{
	RED=1,
	YELLOW,
	BLACK=10,
	GREEN,
	BLUE=100
};

int main()
{
	enum color c = RED;
	printf("%d\n", RED);
	printf("%d\n", YELLOW);
	printf("%d\n", BLACK);
	printf("%d\n", GREEN);
	printf("%d\n", BLUE);
/*
运行结果:
1
2
10
11
100
*/

	return 0;
}

使用枚举可以很好的描述事物需要的特性(状态),尤其是含有多个,不适合使用 define

而且使用枚举可以很好的避免"魔鬼数字"问题,因为使用枚举可以很清晰的表名这个一个枚举变量,而不是像数字一样,如果没有注释说明,他人阅读代码很难明白这个数字的含义,可以增加 自描 属性

枚举与宏定义的区别

如果在项目中常量使用不多且相关性较低,就使用宏定义,否则就可以选择使用枚举

  • #define 宏常量是在编译阶段进行简单替换,枚举常量则是在编译的时候确定其值
  • 一般在调试器中,可以调试枚举常量,却不能调试宏常量

如果对枚举常量进行赋值,则其后是连续赋值的,也可以实现阶段性赋值

#include<stdio.h>

enum color
{
	RED=1,
	YELLOW,
	BLACK=10,
	GREEN,
	BLUE=100
};

int main()
{
	enum color c = RED;
	printf("%d\n", RED);
	printf("%d\n", YELLOW);
	printf("%d\n", BLACK);
	printf("%d\n", GREEN);
	printf("%d\n", BLUE);
/*
运行结果:
1
2
10
11
100
*/

	return 0;
}

使用枚举可以很好的描述事物需要的特性(状态),尤其是含有多个,不适合使用 define

而且使用枚举可以很好的避免"魔鬼数字"问题,因为使用枚举可以很清晰的表名这个一个枚举变量,而不是像数字一样,如果没有注释说明,他人阅读代码很难明白这个数字的含义,可以增加 自描 属性

枚举与宏定义的区别

如果在项目中常量使用不多且相关性较低,就使用宏定义,否则就可以选择使用枚举

  • #define 宏常量是在编译阶段进行简单替换,枚举常量则是在编译的时候确定其值
  • 一般在调试器中,可以调试枚举常量,却不能调试宏常量
  • 枚举可以一次定义大量相关的常量,而#define宏 一次只能定义一个

缝纫师关键字:typedef 关键字

typedef 的作用就是给变量做重命名

使用方法:

#include <iostream>

using namespace std;

//将关键字名 重命名
typedef unsigned int u_int;

//将结构体 进行重命名
typedef struct stu
{
	char name[16];
	int age;
	char sex;
}stu_t;

int main()
{
	u_int x = 0;
	cout << "关键字重命名:" << x << "\n";

	stu_t s;
	s.age = 22;
	cout << "结构体重命名: " << s.age << "\n";
/*
运行结果:
关键字重命名:0
结构体重命名: 22
*/

	return 0;
}

在实际项目中,要切忌对 变量类型进行过度重命名,这样会大大降低 代码的可读性,这里推荐对结构体进行重命名,而不要对其他类型再进行重命名了

typedef使用的注意事项

在 C/C++ 中要注意,在实际编码过程中,要注意编码规范image
在这里,a 为指针(可以理解为靠 * 更近),b 为整形

//甚至可以这样定义,但是不推荐这样使用
int *a = NULL, b=0;	//a 为指针,b 为整形

但是,如果使用 typedef 进行重命名之后,在使用这个语句进行定义后,就会发现 a,b 均为指针了

image

所以在理解typedef 时,不能简单的将其理解为给该类型取了个别名,而是可以认为 姓名字是 一个新的 类型,这个类型可以对 使用该类型进行定义的变量 进行统一定义

所以在实际开发中,如果要定义指针或是整形,最好单独的 分开 进行定义

typedef 与 #include

typedef 是类型重命名,但本质上并不属于宏 的替换,相当于是一个新的类型,所以在使用这个类型时,和使用 double/float 等没有本质区别,而 如果是 #define ,相当于全局替换,将所有的 该变量 都替换为 宏定义的变量image

在代码中也可以看到,使用 #define 的效果和 使用 int* 效果是一模一样的

在使用 typedef 给变量起别名之后,这个变量就不再是之前的那个变量了,而可以看做是和原关键字效果一样的另一个关键字,并且能力受限

typedef int int32
int main()
{
    //错误写法,会直接报错,在使用typedef进行取别名之后,就无法再继续满足之前该关键字所有的功能了
    unsigned int32 a;
    
    return 0;
}

关于C语言关键字的总结就基本完成了,后面会继续更新其他内容的!(。・∀・)ノ*
感谢观赏,慢慢提高

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值