016+limou+C语言常用的32个关键字

0.前言

本博文是在对C语言有一定深入了解后,对C语言最为主要的32个关键字进行了简要的概述和一些容易被忽略的细节研究,您可以当作学习或复习C语言基础使用(毕竟关键字就是构成C语言语法的基石),也可以提出您所不认同的点。当然,如果您有兴趣的话,可以看看我之前写的C语言入门和C语言深入系列。最后,写文难免会有所遗漏,还望君多上机躬行,莫要过信。不过,我相信您此行是必定有所收获的!

1.C语言关键字

C语言关键字起码有32个(C90),后面又新增加了5个关键字(C99)。

2.C语言程序的存储

C语言生成可执行程序的简单理解:

文本代码 -> 可执行程序(简单理解为二进制文件) -> 在Windows中可以使用鼠标双击.exe启动程序。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <windows.h>
int main()
{
    printf("hello word!\n");
    system("pause");
    return 0;
}//x64的Debug环境

如果在vs2022 Debug模式下运行了上述代码,就会在x64文件中出现Debug文件,其中会有一个.exe文件。如果不需要这些转换后的二进制文件,可以点击“生成->清理解决方案”。
另外:

  • 在程序没有运行的时候,程序先存储在硬盘中(是外存的一种,和内存相对)。
  • 在程序开始运行之前(鼠标双击),必须先把程序加载到内存中,因为这样运行比较快。实际上程序被加载到内存中,就不能叫“程序”了,应该叫“进程”了。换而言之,“全局变量的作用域是随进程的”。

3.变量的定义

3.1.什么是变量

  • 在内存中开辟特定大小的空间,用来保存数据
    • 一般变量只有在程序运行的时候开辟空间
    • 任何程序在开始运行之前(鼠标双击),必须先把程序加载到内存中。换而言之,所有的变量在程序运行之后,就只能在内存的某个位置(具体是什么位置还需要另外讨论)开辟空间了,无法在外存里开辟空间

3.2.怎么用变量

数据类型 变量名 = 初始化值;//定义+初始化

数据类型 变量名;//定义
变量名字 = 赋值值;//赋值

//注意这两种写法细说还是有所区别的,只是结果等价
  • 为什么存在数据类型:
    • 为了对内存进行合理化划分,高效使用变量
    • 数据类型的存在,决定了给数据开辟了一段多大的空间
    • 数据类型的存在,决定了怎么去解释、读取一个空间内的数据
  • 为什么有这么多数据类型:
    • 为了适应不同场景下的数据大小
    • 另外,一条语句可以理解为存在两个类型,一个“数据值类型”,另外一个是“目标变量类型”,例如“float a = 10.1”,其中10.1是“数据值类型”,float是“目标变量类型”

3.3.变量有何用

一台计算机在计算之前,需要数据,但是不是所有数据都要被立马计算的。因此有效数据就需要先保存起来,等待后续处理,而且效率高,这就是变量的意义。

3.4.定义与声明的差别

  • 定义概念:定义的本质是要开辟空间的,只能定义一次
  • 声明概念:声明是告知编译器已有一块空间,可以声明多次

3.5.生命周期、作用域

生命周期描述一个变量的存在时间(“什么时候开辟----什么时候释放”之间的时间)。作用域描述一个变量可以被使用的有效区域。

3.6.变量命名规则(只是建议)

  • 命名的长度要符合“min-length(最小长度)&&max-information(最大信息)”两个原则
  • 给全局变量带上前缀"g_"以表示该变量是一个全局变量
  • 可以尝试匈牙利命名法(少用了),当然最好是使用大小驼峰命名法
  • 不要单纯使用一个大小写字母来区分变量,比如:在一个程序中定义了一个i又定义了一个I
  • 函数名字和变量名字最好不要一样(尽管这样在一些编译器依旧没有问题)
  • 宏名使用大写,空格用“_”代替
  • 循环索引语句直接使用i、j、k来命名是没问题的(这已经约定俗成了)
  • 定义变量一定要进行初始化!!!(强烈建议)
  • 变量在初始化或赋值的时候最好严格定义,加上后缀u、f、l等保证数据值类型和目标变量类型严格一致

3.7.整型变量的存储

实际上存数据的时候才不管是什么变量类型,先把要存储数据按照整形数据和浮点数据存储,最后再根据数据类型来解读数据,这就跟以前“指针变量是根据指针类型来读取数据”这一知识点串联起来了

  • 任何数据在计算机中都必须转化为二进制,整型变量是以补码的形式存储在数据中的
//注意在补码转化为原码的时候,推荐使用方法二,因为更加符合计算机的工作转化,而非使用方法一

//方法一
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 <stdio.h>
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;
}
//数据先按照自己的值类型转为二进制,再按照所给类型存储,再按照所给转化说明读取
  • 在存储的时候实际上还涉及到大小端的问题

3.8.浮点数的存储

这个比较复杂,不在本次多说

3.9.“类型的范围”以及“signed、unsigned关键字”的使用

  • 不要形成所有数据类型都是有符号的这种认知,尽管很多编译器厂商都将部分数据类型关键字都实现为有符号,但是不是所有编译器都会如此
  • 在无符号的情况下,其实挺简单的,但是如果到了有符号就会出项0和-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
  • 而在C内置的其他有符号类型也有类似的情况
    • 如果是仔细探究,就可以根据这个规定进行计算
    • 如果只是单纯计算,可以考虑使用循环法
//题目练习一
char a[1000];
for ( int i = 0; i < 1000; i++)
{
    a[i] = -1 - i;
}
printf("%d", strlen(a));//请问这里会打印多少呢?输出255

//题目练习二
#include <stdio.h>
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 <stdio.h>
#include <windows.h>
int main()
{
    unsigned int i;
    for (i = 9; i >= 0; i--)//无效的循环写法,会陷入死循环
    {
        printf("%u\n", i);
        Sleep(1000);
    }
    return 0;
}

4.变量关键字

4.1.auto

  • auto是“自动存储”关键字,是个比较老的关键字了
  • 一般情况下局部变量默认是auto的,auto一般只能修饰局部变量,不能修饰全局变量
  • 在概念上“局部变量==临时变量==自动变量”
  • 但是一般会把局部变量前面的auto省略,因此这个关键字现在在C编程中基本是不用了(但是在C++中还是有用的!)

4.2.register

  • 一些硬件的了解
    CPU是主要负责计算的硬件,但是其内部也是存在可以临时存储数据的地方的,即:寄存器。这样不需要从内存中读取数据,CPU的计算速度会更快

常见的数据存储硬件
寄存器
缓存:L1cache/L2cache/L3cache
内存:DRAM芯片
硬盘:HDD/SSD/flash
光盘
软盘
磁带
离CPU越近的越贵,速度越快

  • 什么样的变量可以使用寄存器关键字
    • 必须是局部的(全局变量会导致CPU寄存器被长时间占用)
    • 不会被写入的(写入就需要写回内存(比如i++,需要给CPU计算+1后写回内存),后续还要读取检测的话,那register的使用就没有意义了)
    • 高频被读取的(提高效率所在)
      如果要使用,也请不要大量使用,因为寄存器数量是有限的
  • 使用寄存器变量注意
    • 由于寄存器变量放在寄存器而不是内存当中,不能对寄存器变量进行取地址操作(哪怕后续再进行赋值也不可以)
    • 与其说是声明用的关键字,倒不如说是申请用的关键字。register关键字修饰变量不是使用了就会把该变量变成寄存器变量(因为寄存器的数量是有限的),编译器会有自己的判断,认为这个变量是否合适成为register变量

4.3.头文件与static和extern

4.3.1.头文件的使用

  • 所有变量只是声明的时候,不能设置初始值,因为声明没有开辟对应的内存空间(除了在定义的时候)
  • 一开始只有多份源文件的时候,想要使用一份特殊的源文件中的变量/函数,就需要不断地在不同地源文件里对它们声明,才能正常使用
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test1.c文件内部
#include <stdio.h>

extern int c;//声明外部变量
extern void test(void); //声明外部函数
int main()
{
    printf("%d\n", c);//使用外部变量
    test();//使用外部函数
    return 0;
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test2.c文件内部
#include <stdio.h>
int c = 100;
void test(void)
{
    printf("limou\n");
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
  • 但是这样做无疑会增加代码维护地成本,因此诞生了头文件,需要使用某个文件内的变量/函数,只需在头文件内部声明变量/函数,在其他源文件被包含,就可以使用这些变量/函数,这样就可以减少大型项目地维护成本
  • 防止头文件被重复包含的方法有两种,最简单的是使用#pragma once
  • 头文件的内容一般是变量和函数声明、宏定义、结构体定义、typedef
  • 在头文件中变量声明必须带上extern(因为有可能被识别为定义),函数声明则不会(因为函数是定义还是声明取决于是否有函数体,但是依旧使用便于阅读文档)
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在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");
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

4.3.2.static

4.3.2.1.修饰全局变量(改变作用域)

使得变量变成静态全局变量,其作用域仅限于该变量被定义的地方开始,因此该变量不能被直接跨文件使用(但是可以间接使用)

//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test1.c文件内部
#include <stdio.h>

extern int c;//声明外部变量
extern void test(void); //声明外部函数
int main()
{
    printf("%d\n", c);//使用外部变量,但是无法直接使用了
    test();//使用外部函数,可以看到test函数调用了其定义所在文件的静态变量,这个就是间接使用
    return 0;
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test2.c文件内部
#include <stdio.h>
static int c = 100;
void test(void)
{
    printf("limou:%d\n", c);//静态全局变量只能在本文件被直接使用
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
4.3.2.2.修饰函数(改变作用域)

使得函数变成静态函数,其作用域仅限于该函数被定义的地方开始,因此该函数不能被直接跨文件使用(但是可以间接使用)

//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test1.c
#include <stdio.h>

extern void test(void);//声明外部函数
extern void (*ptest)(void);//声明外部函数指针变量
extern void fun(void);//声明外部函数
int main()
{
    test();//直接使用外部函数,没有办法直接被使用
    (*ptest)();//通过函数指针间接使用,调用函数成功
    fun();
    return 0;
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
//在test.2
#include <stdio.h>
int c = 100;
static void test(void)//被static修饰
{
    printf("limou:%d\n", c);
}

//间接使用方法1
void (*ptest)(void) = &test;
//间接使用方法2
void fun(void)
{
    test();
}
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
4.3.2.3.修饰局部变量(改变生命周期)

和上面的全局变量和函数的使用意义有些不同,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;
}
4.3.2.4.C程序地址空间

那么局部变量被static修饰后改变了生命周期的原因是什么呢?本质就是static修饰的局部变量从“栈区(临时性)”转移到了“全局数据区”,但是注意这是操作系统的概念,并不是C语言中的概念

在这里插入图片描述

4.3.2.5.static的其他作用?

在C++中,static还有一个作用,但是这就另说了

4.3.3.extern

这个关键字比较好理解,就是声明一个变量/函数,尽管函数的声明不需要依靠extern,但是就代码整洁规范来说,最好还是要加上extern来显示声明

5.sizeof关键字

实际上sizeof是一个关键字/操作符,而不是一个函数,其作用是计算变量类型大小,使用的单位是字节,在编译期间就起效果了

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
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的使用中,类型必须加括号,因此上面四条语句只有前三句是正确的

6.if和else关键字

6.1.一种巧妙的注释方法

可以使用if注释,当然有点好笑,但是没问题不是么,但是不推荐这种方式,但是在以前是真的有程序员写出类似的“代码注释”的,只需要知道有这种方式就可以了

if(0)//因为没有机会被编译运行
{
	某些代码或注释
}

6.2.C语言中bool的讨论

bool变量在很多高级语言都有,有两个取值true和false,一般来说C语言在C89、C90中是没有bool类型的,但是C99引入了_Bool类型,在新增的头文件stdbool.h中,又被重新用宏写成了bool,这是为了保证C和C++之间的兼容性(注意和C不同,C++中本就有bool值的)

#include <stdio.h>
#include <stdbool.h>//bool的头文件
#include <windows.h>//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个字节。

6.3.if判断条件的写法

#include <stdio.h>
#include <stdbool.h>//bool的头文件
#include <windows.h>//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;
}

6.4.if的执行过程

计算if判断语句中的真假
值判定真假值
进入分支语句

6.5.浮点数的条件判断

  • 由于浮点数的存储不是完整存储而是有精度损失,因此会出现下面的问题

在这里插入图片描述
在这里插入图片描述

  • 因此浮点数不能直接使用“==”来比较,因此浮点数也没有办法和0比较,因此一般使用比较的方法
  • 而如果要比较两个浮点数,可以使用范围的方法,控制精度(epsilon即“小的正数”,即“ɜ”),只要保证两个浮点数的差的绝对值在精度之内就可以
if ( ((x - y) > -精度) && ((x - y) < 精度) ) 
if ( (fabs(x - y) < 精度) )//注意需要使用math头文件才可以使用函数fabs
  • 而实际上C语言本身是有最小精度定义的(这是当前C与能达到的最小精度)
#include <float.h> //可以使用这两个精度 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都不变)

6.6.指针的判空

//写法一:最为标准的写法,清晰明了
int a = 10;
int* p = &a;
if( NULL == p )//判空的较好写法
{
    //…某些语句
}

//写法二:( p == 0 ),容易误解pa为整型

//写法三:( !p ),容易误解p为bool类型

6.7.就近配对原则

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);
    }
}

7.switch、case关键字

7.1.使用场景和缺点

在分支过多的时候可以使用switch,缺点是对于范围判断比较难处理,对于default选项建议还是加上

7.2.case后值的要求

只能是整型常量或整型表达式,并且在C语言里也不能放const修饰的变量

7.3.case语句的顺序

一般来说常用的选项放在前面,不常用的放在后面

7.4.在case和break之间定义变量

在一个case语句的内部中,不支持在多条语句中直接放入定义语句,当时若是写成代码块的形式就支持

#include <stdio.h>
//写法一
  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;
}

7.5.在case语句内部尽量不要使用return

如果代码很多,使用return可能会被误认为是break,不好维护代码

7.6.在switch语句里最好不要使用布尔值

不然会出现能到达某个case语句的情况

7.7.在case中的语句应该尽量简短

7.8.正确使用default

不应该把某些选项的情况交给default来处理(即偷懒把某个case改造成default)…

7.9.switch使用到的关键字所扮演的作用

switch作为分支语法提示
case完成判定功能
break完成分支功能
default完成异常情况处理

8.do、while、for关键字

8.1.这三个关键字使用起来还是比较简单的

有关C语言内三种循环的详细使用在这里就不多讲了

8.2.contiune关键字

  • while和do while是挑到条件判定处
  • 注意continue在for里是是跳转到条件更新处

8.3.break关键字

就是直接跳出循环体

8.4.go to关键字

实际上在现实生活中,大型项目也有大量使用go to语句,并不是想象中的没人使用

9.void关键字

9.1.void不能定义变量

  • 原因是编译器对void进行了特殊处理,无论不同平台的void是多大,都设置为“不能用void来定义便变量”,直接从约定角度上就规定了这一点
#include <stdio.h>
int main()
{
    printf("%zd\n", sizeof(void));
    return 0;
}
  • 在VS2022来说,sizeod(void) == 0
  • 在Linuc环境下的gcc编译器来说,sizeof(void) == 1

9.2.void不能用于强制转化数据

(void)i;//不合法

9.3.void不能开辟空间

作为空类型,理论上是不应该开辟空间的,即使开辟了空间,也仅仅作为一个占位符来看待,因此不能使用void来创建void类型的变量

9.4.void的作用

  • 作为函数返回值
    • 占位符,让用户知道函数不需要返回值
    • 告知编译器,函数的返回值无法接收
  • 作为函数参数列表
    • 告知用户和编译器函数不需要传参,如果依旧传参,编译器会发出警告或直接错误,若是没有加上void有可能不报错误

在这里插入图片描述

  • 作为void指针
    • 虽然void指针不能定义变量,但是void可以,这是因为指针的大小是固定的,一般来说非4即8,因此void变量就需要4/8个字节来存储数据,但是在解引用的时候,不可以直接解引用。
    • void类型的指针可以被任意类型的指针接受,void类型的指针可以接受任意类型的指针(后者最常用,比如在库、系统接口的通用接口吗设计上会被大量使用)
    • void指针没有办法加减整数,原因是void指针不明确指向的类型大小,没有办法加减整数(这是在VS2022中的结果,但是linux下的gcc可以编译通过)
      在这里插入图片描述
    • void指针解引用无论在哪一个平台都是不能进行解引用的,如果能解引用就产生了一个void类型的变量,而void变量是不存在的
      在这里插入图片描述

10.return关键字

10.1.不要返回栈内存指针

不要返回指向“栈内存”的“指针”,因为该内存在函数体结束的时候会被自动销毁,访问这个指针指向的内容这将带来风险

int fun()
{
  	int number = 10;
  	return &number;
}
int main()
{
  	int* i = fun();
  	printf("%d", *i);//打印出乱码
	return 0;
}

10.2.return能返回值的本质原因

明明局部变量被销毁了,为什么还能返回值呢?

int fun(void)
{
    int i = 10;
    return i;//return把“变量/表达式”的值被放进了CPU的寄存器里
}

int mian()
{
    int y = fun();//将寄存器里的值放入y中,如果不拿y接收就不对寄存器里的值做处理
    return 0;
}

因此,“被调用函数”的返回值是通过寄存器的方式返回“函数调用方”的

10.3.“return;”的使用

是一个空的 return 语句,常用于函数的最后,用于结束函数的执行并返回调用者。这里的分号表示语句结束的标志,表明函数的返回值为空

10.4.函数的返回值具有常量属性(以后说)

10.5.main函数的返回值去哪里了?(以后说)

11.const关键字

11.1.基本要点

  • const的作用
    • 修饰变量时,赋予变量只读属性。即被const修饰的变量,不能被“直接”修改,但是可以“间接”修改(这是一种弱约束)
    • 系统真正意义上的强约束是指例如“不允许对字面常量进行修改”等操作
int main()
{
    char* p = "hello word!";
    *p = 'H';//这是不允许被直接修改的,也不允许间接修改
    return 0;
}
  • const的位置

实际上放在类型前和类型后是没有区别的

int const i = 10;//可以这么写但是不推荐
  • 注意被const修饰后的类型和原类型在编译器看来可能不是一个类型

    • 一般是弱限制到强限制可能没有错误,强限制到弱限制可能会有警告
  • const的意义

    • 给编译器看:让编译器将不会直接修改某个变量(比如写代码的人对变量进行了误操作),在编译期间保护代码,而不是运行
    • 给程序员看:让程序员知道某个变量不能被修改
    • const只能在定义的时候进行初始化,不能通过二次赋值
const int a; 
a = 10;//这是不被允许的

11.2.将const修饰的变量设置为数组大小

有的环境编译不过(VS2022),有的环境编译得过(Linux下的gcc编译器),但是如果向标C看齐的话,就是不可以

11.3.修饰数组

修饰数组时就是把数值变成只读数值,即:每个元素都是只读得

11.4.修饰指针(重要)

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 const p”中const关键字修饰的是“”,表示后续的“内容(p)”指向的内存空间中的值是不可通过“内容(p)”修改的(注意这里的const不是修饰关键字int)。而const修饰变量的话,就是这个变量不能被修改
  • 在使用多级指针的时候上面这一点极为突出
	//二级指针	
	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;

11.5.在函数的形参内使用const

实际上这是一种预防性的编程,也是const用的最多的地方

11.6.在函数的返回值使用const

const int* GetVal()//这样返回的指针不会在调用函数内被直接修改(尤其是那些需要修改字符串然后返回字符地址的代码)
{
  	static int a = 10;
  	return &a;
}
int main()
{	
  	const int* p = GetVal();
  	//*p = 100;//不合法
	return 0;
}

实际上内置类型返回,加上const是没有什么意义的(本例子是指针类型,并不是内置类型)

12.volatile关键字

12.1.基本概念

直白翻译就是易变的、不稳定的意思,被这个关键字修饰的变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其他线程等。遇到这个关键声明的变量,编译器对访问该变量的代码不再进行优化,从而提供特殊地址的“稳定访问”(即“不用volatile修饰的代码有可能会被编译器优化”)。
另外在Java中也有个类似的并且还多一个“指令重排”的功能.
在应用开发、单进程的程序中是基本不会用到volatile字的。

volatile int i = 1;//这里的代码会导致代码不会优化(代码会被编译器优化修改,变得不再从内存读取数据i的内容(因为是死循环代码)),而保持内存的可见性(被CPU看到)
int main()
{
  	while(i);//这个语句编译器有可能进行优化,这在反汇编的时候可以查看一下代码变化
	return 0;
}

以上代码可以在Linux中测试其反汇编,来查看volatile的影响。

12.2.const和volatile一起使用

由于翻译的原因,如果这两个关键字共用,很有可能会有人误会这里两个关键字没办法共用(无法修改易变的)。
但是实际上const是要求不进行写入(考虑写的问题),volatile是要求每次读取数据的时候都要从内存读取(考虑读的问题),因此两者并不冲突。

13.结构体关键字

在现实场景中仅靠int、float是不够的,因此产生了结构体的概念,C努力将结构体和变量的行为从应用的角度上一样(例如不像传数组只能传地址,结构体可以传值)

13.1.使用struct定义结构体类型

13.1.1.定义一个struct结构体

struct 结构名字
{
 	结构体成员1;
  	结构体成员2;
	结构体成员3;;
};//注意不要忘记这个分号!!!

13.1.2.struct初始化与赋值

值得注意的是,只能在定义的时候进行初始化,不能在定义后进行赋值(如果一定要这么做,只能通过结构体成员访问符“.”和“->”来做到)

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 <string.h>
//如果关于strcpy函数的使用出现警告,则可以采用语句:#pragma warning(disable:4996)忽略掉错误
{ 
	int data; 
	char str[10]; 
}; 
int main() 
{
	struct Datas a;
	a.data = 1;
	strcpy(a.str, "abcd");
	return 0; 
} 

13.1.3.结构体指针

注意结构体指针在数值等于其成员的最小地址

13.1.4.空结构体大小

和环境有关,没有准确的答案。甚至在有的编译器里直接就不允许定义空结构体(比如VS2022编译器),而有的编译器会认为空结构体的大小是0,如果拿它定义变量就会得到大小为0的变量,注意这也是由编译器决定的,莫要记死!

13.1.5.柔性数组

C99标准新增加的功能(跨平台性可能不太好),柔性数组实际上是为了方便动态开辟内存空间而设计的,其只能在结构体内部定义,且一般放在结构体成员的最后一个

struct str
{
  	int arr[0];//并且不占一个结构体的大小,只有在动态申请内存的时候才会显现出来
};
//但是直接定义一个数组大小为0这是不被允许的
int main()
{
  	int arr[0];//不合法
  	return 0;
}

以下是柔性数组的具体使用

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
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 <stdio.h>
#include <string.h>
#include <stdlib.h>
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两次,次数越多,错误率越高。

13.1.6.位段/位域

以后补充

13.2.使用union定义结构体类型

联合体是一种对数据存储的解决方案。
联合体的内存是共用的,并且大小端对它的影响是比较大的。
联合体大小不能小于最大成员的大小,并其起始位置和其所有成员的起始地址都是一样的(在数值上),即:每一个变量从最低地址处一起共用同一块内存。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
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 <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef union Un
{
	int i;
	char j;
}Un;
int main()
{
	Un a;
	a.i = 1;
	printf("%d", a.j);//输出1的话就可以判断运行机器是小端机器
}

13.3.使用enum定义结构体类型

enum的含义就是“枚举”,可以创建枚举常量,具体使用如下:

enum color
{
  	RED;
  	YELLOR;
  	BLUE;
};

尽管枚举类型可以理解为int类型,都是还是有区别的!枚举定义的变量初始化和赋值最好还是取结构体内部定义的内容,而不应使用整型直接初始化或赋值。
枚举变量使得整型变量携带上一些文本信息(具有自描述性),这些信息是供人类阅读的(也就是说提高了代码的可读性),对计算机来说是没有区别的。
如果使用宏也可以,但是对比enum来说,枚举常量会更加方便,而且会做语法检查,因此在大型项目中枚举的使用率还挺高的。

14.typedef关键字

14.1.typedef的基本使用和好处

用来给类型重命名,并不是创建一个类型,typedef的使用可以规范化代码和简化类型名,但是注意过多的使用有可能造成阅读困难。

//C语言定义数组的方式其实很奇怪,比如int arr[3]数组的类型是“int[3]”,指向函数void fun(void)的指针p的类型为“void(*)(void)”,但是按照定义变量的规则,正常来讲应该是“类型+变量名”的顺序,而上述提到的类型都是将变量名杂糅在类型中,而typedef就可以避免这些现象
#include <stdio.h>
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;
}

14.2.typedef和define的区别

typedef和宏的对比最大的地方在于连续定义多个变量的时候,下面代码揭示了typedef是重命名一个类型,而不是直接替换类型

typedef int* ip;
ip a, b;//a,b都是int*类型

#define int* IP;
IP c, d;//c是int*类型,但是d是int类型

在其他关键字对两者进行修饰的时候也会有所区别

#include <stdio.h>
#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中报错是“指定了一个以上的存储类”

15.一些补充点

15.1.标准输入、标准输出、标准错误

任何一个C程序再运行的时候i都会打开标准输入、标准输出、标准错误这三个流。

15.2.printf的返回值

printf的返回值是输出到屏幕上的字符个数(这个时候就会发现printf输出到屏幕上的东西都是字符!!!这也是为什么叫格式化函数的原因,将数据格式化输出到屏幕,而键盘和显示器都可以叫“字符设备”)。

15.3.计算机的删除数据

计算机的删除数据并不是重置数据为某个数或者清空数据,而是直接设置该数据无效,所以有的时候我们可以看到删除数据的速度要比传入数据快很多。

15.4.标准C的编写风格

此我们应尽量使用标准C的编写方式才能使得代码具有强跨平台的特性。

15.5.内存的编址

内存中的编址是不需要开辟空间存储的,是通过硬件电路的方式对内存进行编址。

15.6.左值与右值的理解

int x;

x = 100;//x的空间,侧重x变量的属性,左值
int y = x;//x的内容,侧重数据的属性,右值

//任何的变量名,在不同的应用场景中有可能代表不同的含义

15.7.“指针”和“指针变量”概念混谈现象

一是翻译原因,二也有可能是左值和右值的原因。

15.8.取地址的方式

对任何变量取地址“&”都是从最低地址开始。

15.9.解引用的本质

(类型相同)对指针进行解引用代表的就是指针所指向的目标。

15.10.调用函数形成形参

C语言中任何函数参数都一定会形成临时变量,包括指针变量。

15.11.声明的本质

注意声明是没有开辟空间的,extern声明的变量只是声明。

16.关键字总结(32个)

16.1.数据类型关键字(12个)

整型 char short int long
浮点型 float double
有无符号 signed unsigned
自定义类型 struct union enum
空类型 void

16.2.控制语句关键字(12个)

循环控制 for do while break continue
条件语句 if else goto(无条件跳转语句)
开关语句 switch case default
返回语句 return

16.3.存储类型关键字(5个)

注意存储关键字是不能放在一起使用的
autoextern register static typedef
(typedef关键字也被分到存储关键字分类中,虽然看起来没有什么关系)

16.4.其他关键字(3个)

const sizeof volatile

17.末尾

由于目前最为常用的标准还是C90,C99的以后再补呈给您罢,祝君共勉

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

limou3434

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值