c语言入门(3万字详解)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: c语言入门(3万字详解)

在本文中,对每个知识点做简单认识,不做详细讲解,先基本了解c语言的基础知识,对c语言有一个大概的认识。

一:什么是c语言?

  • C语言是一种通用的、面向过程的编程语言。它由美国贝尔实验室的Dennis Ritchie于1972年开发,并在之后的几十年里成为了一种广泛使用的编程语言。
  • C语言以其简洁、高效和可移植性而闻名。它提供了丰富的编程结构和库函数,可以用来开发各种类型的软件,从嵌入式系统到操作系统,从游戏到商业应用。
  • C语言的语法相对简洁,它使用函数和变量来组织代码,具有良好的可读性和可维护性。同时,C语言的执行速度也非常快,因为它直接操作计算机的底层硬件。
  • 由于C语言的广泛应用和丰富的工具与库支持,它成为了许多其他编程语言的基础,包括C++、Java和Python等。学习C语言对于了解计算机底层原理和进行系统级编程非常有帮助,也是学习其他高级编程语言的良好起点。

二:第一个c语言程序

#include <stdio.h>
int main()
{
  printf("hello bit\n");
  printf("he he\n");
  return 0;
}

在此,我简单解释一下这段代码的意思

  • 在我们写代码的时候,有些功能会频繁被我们使用,为了方便我们更加高效的写代码,每个编译器都提供了许多库,每个库中又有许多函数,一个函数实现了对应的一个功能,比如说:printf()这个函数的功能是将数据按照指定的格式输出到屏幕上。()中放的是函数的参数,但是printf()这个函数是库中的函数,它在stdio.h这个库中定义,所以我们要想使用printf()这个函数就需要将这个库导入程序中.
  • #include <stdio.h>就是将stdio.h这个库导入程序,这样我们才能够使用程序中的printf()函数
  • printf(“hello bit\n”);就是在屏幕上打印hello bit并且换行,\n就是换行的意思,\n是转义字符,并不会打印出来,同理,printf(“he he\n”);就是在屏幕上打印he he并换行
int main()
 {
 return 0;
   }

以上是主函数的基本格式,main()叫做主函数,它是一个特殊的函数,主函数有且仅有一个,它是程序的入口点,C 语言程序的执行从主函数开始,也在主函数结束时终止,其中int代表main()的返回值类型,return 0;代表的是main()的返回值是0,在 C 语言中,主函数可以不需要接收任何参数,所以()中可以留空。

主函数中的 return 0;表示程序执行成功,并返回给操作系统一个退出码,通常约定成功返回0,其他非零值则表示程序执行出现错误。操作系统可以根据这个退出码来判断程序的执行状态。

这段代码的运行结果如图所示:

如果不换行的话就是这个结果了

三:数据类型

c语言中数据类型有以下7种

char 字符型
short 短整型
int 整型
long 长整型
long long 更长的整型
float 单精度浮点数
double 双精度浮点数
  • char是描述字符的
  • short long int long long是描述整型的
  • float double是描述浮点型的

那什么又是字符,整型,浮点型呢

  • 字符就是我们在键盘上打出的除数字外的任意一个符号,字符的表示需要在字符的基础上加上单引号’ ‘,比如’a’ ‘t’ ‘!’ ‘@’ ‘~’ ‘3’
  • 整形顾名思义就是整数类型的数据,也就是整数
  • 浮点型就是小数,小数被称为浮点数或浮点型是因为它们在内存中的表示方式是通过使用浮点表示法。

那么为什么我们对整型和浮点型的描述需要这么多类型呢?

主要原因是因为我们需要对内存空间进行合理的分配,short long int long long虽然都是描述整型的,但是他们的取值范围不一样,在内存中所占的空间大小也就不一样,选择使用哪种整型类型取决于你对内存空间和数值范围的需求。如果你需要处理的数值较小,可以选择使用 short 或者 int 类型,而如果需要处理的数值范围较大,你可以选择使用 long或者long long 类型。

同理,浮点型也一样,double的精度更高,但是占用的内存空间会更大,而float的精度低,但是占用的内存空间比较小,需要说明的是,浮点型在内存中是无法精确存储的,这与浮点型在内存中的存储方式有关,只能对浮点型的精度进行控制,控制误差,无法准确的存储浮点型。

3.1数据类型的大小

数据类型 类型名字 内存中占用的空间大小
char 字符型 1个字节
short 短整型 2个字节
int 整型 4个字节
long 长整型 4个或者8个字节
long long 更长的整型 8个字节
float 单精度浮点数 4个字节
double 双精度浮点数 8个字节

在此我们需要了解一下内存存储的单位

1个字节等于8个二进制位,而一个千字节又等于1024个字节,一兆等于1024个千字节………以此类推

四:变量,常量

变量即稳定不变的量,而常量就是会发生改变的量,常量和变量的概念都源于生活,生活中有些东西是不会改变的,比如说圆周率,血型等,当然生活中也存在一些会改变的东西,比如说年龄,体重等……

4.1定义变量的方法

int age = 150;
float weight = 45.5;
char ch = 'w';

在此我解释一下 int age = 150;这段代码的意思:

  • int是数据类型,表示age的类型是int
  • age则是变量的名字,变量的名字你可以随便取,最好要有实际意义
  • =是赋值符号而不是等于,赋值符号是从右向左执行的
  • 这段代码先声明了一个变量int age 接着通过=这个赋值符号,从右向左将150赋给了age 这个变量,需要注意的是,赋给age的值要与age的类型对应上,
  • 需要注意的是字符w应该写成’w’而不是w,

4.2变量的命名

  • 只能由字母(包括大写和小写)、数字和下划线(_)组成。
  • 不能以数字开头。
  • 长度不能超过63个字符。
  • 变量名中区分大小写的。
  • 变量名不能使用关键字。

变量的命名字符不能超过63个字符是因为在大多数编程语言中,标识符的长度都有一定的限制。这是由于编程语言的解析器和编译器在处理代码时需要预留一定的内存空间来存储变量名。超过限制的字符数可能导致内存溢出或其他运行时错误。此外,过长的变量名也会降低代码的可读性和可维护性。因此,建议在命名变量时坚持简洁、清晰和有意义的原则,同时遵循编程语言的命名约定。

变量名区分大小写的意思就是说Abandon和abandon是两个不同的变量名,ABandon和abandon也同样是两个不同的变量名

C语言中的关键字是具有特殊含义的标识符,它们被用于特定的语法结构和功能。比如说int short long等都是关键字,不能将关键字拿来命名

4.3变量的分类

变量分类一般有两种

  • 局部变量
  • 全局变量

在了解局部变量和全局变量前,我们先了解一下什么是变量的作用域和生命周期

  • 作用域
    作用域是指在程序中某个特定位置声明的变量所能被访问的范围。作用域规定了变量的可见性和可访问性。在不同的编程语言中,作用域可以分为全局作用域、局部作用域等不同的类型,变量在其作用域内可以被引用和修改,而在其作用域外则无法访问。
  • 生命周期
    生命周期是指变量存在的时间范围,即变量从创建到销毁的整个过程。每个变量都有其特定的生命周期,它取决于其声明的位置以及该变量是在栈上还是堆上分配的。

那么我们如何知道一个变量是局部变量还是全局变量呢?

#include <stdio.h>
int global = 2019;
int main()
{
  int local = 2018;
  printf("global = %d\n", global);
  return 0;
}

一个变量的作用域就是最靠近这个变量的左右括号所括起来的范围,如果有括号将变量括起来了,那么这个变量就是局部变量,反之则是全局变量

例如:

上述代码中的local就是一个局部变量,它的作用域是主函数,而global则是一个全局变量,它的作用域是整个项目

  • 局部变量的作用域是变量所在的局部范围
  • 全局变量的作用域是整个项目

变量创建的本质:先在内存中开辟一块空间,存放数据,出了作用域变量就无法使用了,当变量的生命周期结束了,就会被销毁(即被操作系统回收,不是将内存给销毁了,只是将内存还给操作系统了)

注意不要在一个项目中定义两个重名的全局变量,他们的作用域都是整个项目,如果重名了,则会报错

但是有时候我们可能会很粗心,不小心将全局变量和局部变量的名字重名了,c语言允许这种情况发生吗?

#include <stdio.h>
int global = 2019;
int main()
{
  int local = 2018;
  int global = 2020;
  printf("global = %d\n", global);
  return 0;
}

很明显,两个global的作用域在main函数中都存在,那printf()打印的结果是什么呢?是否会报错呢?

让我们看看这段代码的运行结果:

我们发现程序运行良好,没有报错,并且global打印的值是2020,那么我们可以得出一个很重要的结论

  • 当局部变量和全局变量同名的时候,局部变量优先使用

4.4 printf()和scanf()

4.4.1printf()

printf()用于格式化输出,是一个C语言中的标准库函数,它被定义在stdio.h头文件中。它的全名是"print formatted"(格式化打印),当我们用printf()函数时,需要用#include <stdio.h>导入stdio这个库才能使用printf()这个函数,下面通过一个例子来说明printf()函数的作用以及使用方法

#include <stdio.h>
int main()
{
  int global = 2020;
  printf("global = %d\n", global);
  return 0;
}
  • printf()中()放入函数的参数,
  • “global = %d\n”:这是一个格式化字符串,它包含了占位符“%d"。这个占位符用来表示一个整数(decimal),它将被替换为变量global的值。"\n"表示换行符,用于在输出中创建一个新的行。
  • global则是%d,这个占位符被替换的值
  • 替换完后将字符串打印在屏幕上

这段代码的输出结果是:

字符串就是一个个字符组成的集合,为了和其他数据区分开来,需要用双引号" “和别的数据类型区分开来 ,比如"1456346346” “asfih325” "sfafkjh"等都是字符串

(1)占位符

接着我们来了解一下printf()函数中的占位符

%c 字符的占位符
%s 字符串的占位符
%d 整型的占位符
%f float的占位符
%lf double的占位符
%p 地址的占位符

下面给出部分使用用例:

第一个:%c

#include <stdio.h>
int main()
{
  char ch = 'A';
  printf("Character: %c\n", ch);
  return 0;
}

输出结果:Character: A

第二个:%s

#include <stdio.h>
int main()
{
  char str[] = "Hello";
  printf("String: %s\n", str);
  return 0;
}

输出结果:String: Hello

第三个:%d

#include <stdio.h>
int main()
{
  int num = 42;
  printf("Integer: %d\n", num);
  return 0;
}

输出结果:Integer: 42

第四个:%f

#include <stdio.h>
int main()
{
  float f = 3.14;
  printf("Float: %f\n", f);
  return 0;
}

输出结果:Float: 3.140000

(2)printf()函数设置精度,宽度,以及对齐方式

在使用C语言中的printf函数进行格式化输出时,可以使用格式控制符来指定宽度、精度和填充字符。

  • 指定宽度:使用数字指定输出的最小字符数。例如,%5d表示输出至少5个字符宽度的整数值,如果不足5个字符,则在左边用空格填充。
  • 指定精度:对于浮点数,使用.n来指定小数位数,其中n为希望输出的小数位数。例如,%.2f表示输出2位小数的浮点数。
  • 填充字符:使用%[填充字符]来指定填充字符,将填充字符放在宽度和格式化字符之间。例如,%06d表示使用0作为填充字符,在输出宽度不足时用0进行填充。

下面是一个示例:

#include <stdio.h>
int main()
{
  int num = 42;
  float pi = 3.14159;
  printf("%5d\n", num);   
  printf("%-5d\n", num);  
  printf("%.2f\n", pi);  
  printf("%06d\n", num); 
  return 0;
}

运行结果如图所示:

4.4.2scanf()

scanf()是一个C语言中的标准库函数,用于输入数据,它的主要作用是将用户输入的数据存储到变量中,以供程序进一步处理或使用。它被定义在stdio.h头文件中,当我们用scanf()函数时,需要用#include <stdio.h>导入stdio这个库才能使用scanf()这个函数。

(1)scanf()函数用法

用法:scanf(format, variable);

  • format是一个格式化字符串,用于指定输入数据的类型和格式。
  • variable是要存储输入数据的变量,需要提供变量的地址(即使用&运算符)。

下面通过一个例子来演示scanf()函数的用法

#include <stdio.h>
int main()
{
  int num = 0;
  printf("请输入一个数字赋值给num:");
  scanf("%d", &num);
  printf("num = %d\n", num);
  return 0;
}

运行结果如图所示:

接下来我来解释一下scanf(“%d”, &num);这段代码的意思,

这段代码的意思是先读取用户输入的数据,并将这个数据赋给num这个变量,

  • &是取地址的意思,scanf函数需要变量的地址作为参数,是因为它需要知道变量在内存中的位置才能够将输入的数据存储到正确的变量中。
  • 在C语言中,变量是存储在内存中的一块特定的空间,每个变量都有一个唯一的地址。当我们将变量的地址传递给scanf函数时,它就可以通过这个地址找到变量所在的内存位置,并将输入的数据直接写入到这个内存位置。
  • 如果我们不将变量的地址传递给scanf函数,而是直接传递变量本身,那么scanf将无法确定变量在内存中的位置,也就无法将输入的数据正确地存储到变量中。
(2)scanf()函数的注意事项

1:输入数据时候的分隔符要和scanf()函数中的分隔符一样

比如:

  1. scanf(“%d,%d”&num1,&num2);
    &num1与&num2之间有一个逗号,那么我们在输入两个数据的时候也应该加上逗号,比如:100,200(切记不要加空格)
  2. scanf(“%d,%d”&num1 &num2);
    &num1与&num2之间有一个空格,那么我们在输入两个数据的时候也应该加上比如:100 200(切记不要加逗号)

逗号在这里作为一个分隔符,用于区分两个不同的整数输入。如果你的格式化字符串中指定了逗号,那么在输入数据时必须按照这个格式输入,即在两个整数之间使用逗号进行分隔。

这样做的目的主要有两个原因:

  1. 增加输入的可读性和可预测性:通过使用逗号来分隔输入数据,可以在视觉上清晰地区分不同的整数,并确保输入数据的顺序和格式符合预期。这有助于提高输入数据的可读性和可预测性,减少输入错误的可能性。
  2. 确保输入数据的正确解析:格式化字符串中的逗号告诉scanf函数,在读取输入数据时应该遇到逗号作为分隔点,将输入数据分别存储到对应的变量中。如果输入数据不按照指定的格式进行分隔,例如输入的两个整数之间没有逗号或者有额外的空格,scanf函数可能无法正确解析数据,导致读取错误的整数或发生错误。
(3)scanf()意义:
  • 允许用户从键盘或其他输入设备输入数据,实现与用户的交互。
  • 将用户输入的数据存储到程序中的变量中,以供后续处理。
  • 提供了灵活的格式化字符串,可以根据需求指定各种数据类型的输入

4.4.3printf()和scanf()的返回值

  • printf() 函数的返回值是打印到标准输出流的字符数,即成功打印的字符数。
  • scanf() 函数的返回值是成功读取的输入项的个数,即成功匹配并读取的输入项数量。如果读取失败则返回EOF(也就是-1)

4.5常量

常量一般有三种

  • const修饰的常变量
  • #define定义的标识符常量
  • 枚举常量

4.5.1const修饰的常变量

const int n = 100;

这个代码中,n被const修饰了,所以具有了常属性,n不能被修改,如果后面有对n的值进行修改的代码,那么这个代码会报错但是n还是一个变量,只不过有了常属性了而已,故又称作常变量

const修饰的常变量只是在语法层面限制了变量n不能被直接改变,但是n的本质上还是一个变量,只是具有了常属性

4.5.2#define定义的标识符常量

#include <stdio.h>
#define PI 3.14159
int main() {
    printf("The value of PI is: %f\n", PI);
    return 0;
}

下面我来解释一下这段代码的意思:

  • #define PI 3.14159表示将标识符PI定义为一个常量,在此处的值为3.14159。#define是一个预处理指令,用于在编译代码之前进行文本替换。当我们在代码中使用PI时,它将被预处理器替换为3.14159,就像我们直接写出3.14159一样。
  • printf(“The value of PI is: %f\n”, PI);所以在打印这句话的时候PI会自动被替换为3.14159,并且PI的值无法修改

通过#define定义常量的意义:

  1. 这种定义常量的方式可以让我们在代码中使用可读性更好的符号来表示特定的常量值。在这种情况下,通过使用PI代替直接写出具体的数值,我们能够清晰地表达这个数值是圆周率的近似值。这让数值有了具体的意义,而不只是简单的一个数字
  2. 这种定义常量的方式还有一个好处是,如果我们需要在代码中修改这个常量的值,只需要在#define行上修改一次,而不需要在代码中的所有出现的地方都进行修改。这样可以提高代码的可维护性和灵活性。

4.5.3枚举常量

enum Sex
{
MALE,
FEMALE,
SECRET
};

这段代码定义了一个枚举类型Sex,它包含三个枚举成员:MALE、FEMALE和SECRET。每个枚举成员都表示一个特定的性别选项。枚举常量如果不赋值,那么第一个枚举成员默认为0,接着第二个枚举成员默认为1,接着第三个枚举成员默认为2

在这个情况中MALE的值为0,FEMALE的值为1,SECRET的值为2(程序员是从0开始数数的)

enum Sex
{
MALE = 6,
FEMALE,
SECRET
};

而在这种情况下MALE的值为6,FEMALE的值为7,SECRET的值为8

enum Sex
{
MALE = 6,
FEMALE = 9,
SECRET= 13
};

当然也可以分别对MALE,FEMALE,SECRET分别赋值,在这种情况下MALE的值为6,FEMALE的值为9,SECRET的值为13

(1)枚举常量的注意事项
  • 枚举成员的默认值是从0开始的,并且从上至下依次递增
  • 枚举成员之间是以逗号隔开的,而不是以分号
  • 枚举列完之后有一个分号,千万不要漏了
(2)枚举常量的意义

与#define定义的标识符常量类似,枚举常量也让数字具有了意义,而不只是单纯的一个数字,例如

enum Sex
{
  MALE = 6,
  FEMALE = 9,
  SECRET= 13
};

在这个例子中,我们可以用MALE来代替6来使用,printf(“%d”,MALE);很明显打印结果是6,使用枚举能够让代码更具可读性,因为每个枚举成员都有一个有意义的名字。在代码中使用枚举成员,可以更清晰地表达其含义,而不是使用数字或其他形式的常量

五:字符串+转义字符+注释

5.1字符串

字符串顾名思义结束一串字符,字符串被双引号" "引住

需要注意的是:

'w’与"w"不同,一个是字符w,另一个则是字符串w

并且字符串是以\0为结束标志的,这个标志我们无法肉眼看见,需要通过调试才能发现,当然\0在计算字符串长度的时候是不被计算的,而在计算字符串的内存大小时则会被计算进去

char只能存储单个字符,字符串的存储需要通过字符数组来实现(有个印象即可)

5.2转义字符

转义字符是由反斜杠(\)后面跟着一个特定字符组成的字符序列。它们的存在是为了在字符串中表示一些特殊字符、字符序列或者不可打印的字符。转义字符的目的是用来改变字符原本的含义,让这些字符具有特殊的功能。

因为键盘上的按键都是我们使用最频繁的按键,但是我们的键盘上能打印的字符有限,但是又有一些符号在某些情况下我们可能需要用到比如说29中的9上标,所以有了转义字符,这就是转义字符的意义

以下是常用的转义字符

\? 在书写连续多个问号时使用,防止他们被解析成三字母词
\’ 用于表示字符常量’
\" 用于表示一个字符串内部的双引号
\\ 用于表示一个反斜杠,防止它被解释为一个转义序列符
\a 警告字符,蜂鸣
\b 退格符
\f 进纸符
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符
\ddd ddd表示1~3个八进制的数字。 如: \130 X
\xdd dd表示2个十六进制数字。 如: \x30 0

问题1:写个程序在屏幕上打印一个单引号’,怎么做?

问题2:写个程序在屏幕上打印一个字符串,字符串的内容是一个双引号“,怎么做?

可能有人会觉得这题很简单,不假思索就说出了

  • printf(“%c”,');
  • printf(“%s”");

然而这种写法是错误的:

在C语言中,单引号和双引号都具有特殊的含义。单引号用于表示一个字符字面量,而双引号用于表示一个字符串字面量。

如果你直接将一个单引号或双引号放在 printf 函数的格式字符串中进行打印,编译器会将其解释为字符串的起始或结束,并且期望在引号之后提供相应的内容。这样会导致编译错误,因为你提供的内容是不完整的。

正确的做法:

include <stdio.h>
int main()
{
  printf("%c\n", '\'');
  printf("%s\n", "\"");
  return 0;
}

正确的做法是通过转义字符,通过转义字符来防止程序将单引号解析成一个字符字面量,同理双引号也一样

5.3注释

注释是程序代码中用于给人阅读和理解的文字说明。注释可以被编译器或解释器忽略,不会影响代码的执行。注释的目的是提供代码解释、补充说明、指导团队合作和代码维护等方面的信息

以下的代码的意义:

  • 解释代码:注释可以用来解释代码的逻辑、意图、设计思路等,帮助其他开发人员理解代码。
  • 提供文档:注释可以作为代码的文档,描述代码的用途、参数、返回值等信息,方便其他人使用和维护。
  • 团队合作:注释可以帮助团队成员在合作开发过程中更好地理解、评审和改进代码。
  • 注释对于代码的可读性、可维护性和可理解性都非常重要,是编写高质量代码的重要组成部分

c语言的注释有两种

  • 单行注释: //内容
  • 多行注释: /* 内容 */

对于单行注释来说, // 只能注释掉 // 所在行的代码,注释掉的是一行的代码

而对于多行注释来说, / * */ 可以注释掉两个/ /之间的所有内容,但是要注意多行注释不能够嵌套,即

/ * / * */ */,

因为/*会先和与它最近的 */ 进行匹配所以/ * / * */ */,就相当于只有 */了

六:选择语句 if

if的基本结构:

if (condition) 
{
  // 当条件为真时执行的代码块
}
else
{
//当上述条件不满足时执行这个代码
}

condition是一个表达式,如果condition的值为真,那么则执行 if 中的代码,如果condition的值为假,那么则不执行 if 中的代码,此时如果有else,则执行else中的代码

下面是if的基本使用方法:

#include <stdio.h>
int main()
{
  int coding = 0;
  printf("你会去敲代码吗?(选择1 or 0):>");
  scanf("%d", &coding);
  if(coding == 1)
 {
   prinf("坚持,你会有好offer\n");
 }
  else
 {
   printf("放弃,回家卖红薯\n");
 }
  return 0;
}
  • 这段程序先打印 “你会去敲代码吗?(选择1 or 0):>” 这句话,然后读取用户输入的信息(0或者1)
  • 如果是1,那么coding == 1这个条件为真,所以执行 if 中的代码
  • 如果是0.那么则执行else中的代码

七:循环语句while

while 循环是 C 语言中的一种迭代控制结构,用于反复执行一段代码,直到给定条件变为假。

while 循环的基本语法如下:

while (条件) 
{
    // 循环体,会重复执行的代码块
}

首先,计算条件的值。如果条件为真,则执行循环体中的代码块;然后再次计算条件的布尔值。如果条件仍然为真,则再次执行循环体中的代码块。这个过程会一直重复,直到条件变为假为止。如果初始时条件的值就为假,那么循环体中的代码块将不会被执行。则会教学执行接下来的其他代码。

以下是一个使用 while 循环的示例,它打印数字 1 到 5:

#include <stdio.h>
int main() {
    int i = 1;
    while (i <= 5) {
        printf("%d\n", i);
        i = i + 1;
    }
    return 0;
}
  1. 在上述示例中,我们定义了变量 i 并将其初始化为 1。
  2. 接着,遇到了while循环,先判断条件 i <= 5是否满足,i = 1时很明显满足条件,于是我们打印了i的值,此时 i
    的值是1,所以我们打印1,接着, i +1等于2,并且将这个值赋给了 i ,相当于是 i = 2;(记住,=是赋值符号而不是等于号,并且是将右边的值赋给了左边的值),
  3. 执行完代码块的代码后又进行while中的条件判断,因为此时i等于2,依然满足条件,所以继续执行代码块中的代码,打印2,并且将 i 变成3
  4. 在每次循环迭代中,我们打印变量 i 的值,并递增 i 的值。循环将继续执行,直到 i 的值不满足条件,即 i 大于 5

while 循环可以用在满足某个条件时重复执行某块代码。在编写 while 循环时,务必要确保循环的条件最终会变为假,否则会导致无限循环。在上述代码中,我们通过控制 i 的值来控制条件,没有造成死循环

死循环是指在程序中一个循环条件始终为真,导致循环永远不会终止的情况。也就是说,循环中的代码会一直执行下去,直到程序被手动中断或发生其他异常。循环才能够结束

八:函数

一个函数完成某一个特定的功能,此前我们学习的printf()函数的功能就是将字符串打印在屏幕上,scanf()函数的功能就是读取用户输入的数据,并赋值给某个特定的变量,一个函数完成一个特定的功能,这就是函数

在此我们简单了解函数即可,后续会对函数及其他各个知识点进行深度展开

除了库中已经写好的许多函数,我们还可以自己自定义一个函数,因为库中的函数不可能无穷无尽,将功能覆盖到各个方面,所以这时就需要我们自定义一个函数使用,这大大的加大了程序的灵活性和可扩展性,让我们能够在特定的需求下完成特定的功能

8.1:函数四要素

  1. 函数名:函数名是函数的标识符,用于唯一标识函数并在其他地方调用它。函数名应当具有描述性,能够清晰地表达函数的功能和用途。
  2. 参数列表:函数可以接受零个或多个参数,参数用于传递数据给函数。参数列表定义了函数可以接受的参数的类型、顺序和数量。
  3. 返回值类型:函数可以返回一个值作为函数的输出结果。返回值类型定义了函数返回值的数据类型。函数体中return返回值的类型( return中值的类型 )必须要和返回值类型( 函数名前的类型 )一样。
  4. 函数体:函数体是函数的具体实现。它包含了函数的操作和逻辑,通过一系列的语句来完成特定的任务。

这四个要素共同定义了函数的结构和行为。函数名用于标识函数,参数列表用于传递数据给函数,返回值类型定义了函数返回值的数据类型,函数体用于实现函数的具体逻辑。通过调用函数名并传递相应的参数,可以执行函数体中的代码,并返回一个特定的值作为结果。

8.2:实现两个整型相加的简单函数

接着我们来简单实现一个函数,这个函数的功能用于相加两个整型

  • 因为要对两个整型进行相加,所以我们需要有两个变量,暂且设为x和y,并且x和y都应该是整型的,
  • 函数的名字就起做Add,
  • 因为两个整数相加,结果也应该是整型,所以返回值类型应该是int类型的

所以现在函数的大致样子应该是

int Add(int x,int y)
{
}
  • 接着就是对代码的具体实现了,首先我们应该再定义一个数,这个数应该是整型的,用于接收x和y的和,所以有 int z = x + y;
  • 此时z就是x和y的和了,所以我们现在将z作为返回值返回即可 即return z;

最终函数如下

int Add(int x, int y)
{
  int z = x + y;
  return z;
}

以上便是我们通过分析自定义的一个对两个整型相加的简单函数,因为我们此时没有调用printf()函数,不用引用头文件 #include <stdio.h>

接着我们通过主函数调用一下这个函数,代码如下:

#include <stdio.h>
int Add(int x, int y)
{
  int z = x + y;
  return z;
}
int main()
{
  int a = 10;
  int b = 22;
  int c = Add(a, b);
  printf("c = %d", c);
  return 0;
}

打印结果为

可能有同学对int c = Add(a, b);这一行代码不是很理解,我在这简单解释一下不做过多的展开,以后会深入讲解,

  • 在c语言中,我们调用自定义函数的方式是通过 函数名(参数列表)
  • 举个简单的例子,我们调用Add函数是通过 Add(参数列表)
  • 因为我们在自定义的时候为Add函数设置了两个整型变量,所以当我们用这个函数的时候,也需要用两个整型的变量当作参数进行传递,并且变量顺序和类型都要一致
  • 所以我们调用Add函数应该写 Add(a,b);a和b就是我们传给Add函数的两个参数,Add函数将a的值赋给了x,将b的值赋给了y,然后进行运算,
  • 接着将z作为返回值返回,
  • 此时Add(a,b)的值也就是Add这个函数的返回值了,也就是z,所以此时c就是a和b的和
  • 到此Add函数完成了一个简单的两个整数相加的功能

如果此时我们还需要进行另外两个整数的相加,只需要再调用一次Add即可,这也就是函数很重要的作用之一:

  • 代码复用

九:数组

在生活中,我们有一张钞票,我们可以用手拿着,但是如果钞票多了,变成几十万了,我们该怎么存储呢?我们可以用钱包存储钱,此时钱包里存储就是钱的集合,同理,在c语言中,我们可以用一个int 存储一个整型,那么我们该用什么类型来存储很多个整型呢?

答案是整型数组

那么什么是数组呢?,c语言给出规定:数组是一组相同类型元素的集合,这一句话中强调了两个方面的因素

  • 相同类型元素
  • 集合

必须是一组类型相同的元素并且是集合,才能够叫做数组

9.1:数组的定义

int arr[10] = {1,2,3,4,5,6,7,8,9,10};

这是一个数组的简单定义方式,接下来我来解释一下这行代码的意思

  • 一个变量去掉变量名剩下的就是这个变量所属的类型,比如说 int a = 100,a的类型就是int double b =1.25;b的类型就是double,同理可得,上述数组的类型是int [10],如果数组去掉数组名和[ x ] 的话,剩下的就是数组中所存储的元素
  • 其中 int 表示数组中元素的数据类型为整数,[10]表示数组的长度为10,即数组中可以存储10个整数元素,不能超过10
  • 而我们对数组的初始化需要通过{ }
  • 元素与元素之间需要通过逗号隔开
  • 所以上述数组对数组中的10个元素分别赋值为1,2,3,4,5,6,7,8,9,10

9.2:数组的下标

c语言规定:数组的每个元素在数组中都有一个下标,并且下标是从0开始的(程序员从0开始数数而不是1),并且数组中的元素可以通过下标来访问

int arr[10] = {0,0,0,0,0,0,0,0,0,0};

表示数组中有10个元素,所以下标的范围是0-9(从0开始数(0,1,2,3,4,5,6,7,8,9一共有10个)

我们访问数组中的元素是通过 [ ]来访问的,下面通过一个例子来说明 [ ]的使用

例如:arr[3]

  • [ ]有两个操作数,一个是arr,另一个是3,
  • arr代表要访问的数组
  • 而3代表访问数组中元素的下标
  • 所以arr[3]表示访问arr这个数组中下标为3的元素,
  • 也就是访问arr数组中第4个元素,千万不要误以为是第3个元素

数组的元素如果不初始化的话,默认值是0,并且在部分编译器中int arr[ ]

注意:括号中只能放常量,而不能放变量,在c99标准中引入了变长数组的概念,这时候允许数组的大小是变量,但是这种数组不能直接初始化,需要后面再对这个数组赋值

十:操作符

10.1:算数操作符 + - * / %

加减与我们平常用的加减乘没什么区别,在这就不过多讲解了

10.1.1 :乘号操作符 *

乘号的用法与我们平时所用基本上没区别,只需要注意在c语言中乘号是*而不是x,主要原因是要将乘号和字符x区别开来

10.1.2:除号操作符 /

/ 表示进行除法运算,但是不同于我们平时的除法,c语言中的除法运算有两种,一是整数除法,二是浮点数除法

整数除法:

7 / 2 = 3

8 / 2 = 4

9 / 2 = 4

小数除法:

7.0 / 2 = 3.5

7 / 2.0 = 3.5

7.0 / 2.0 = 3,5

8.0 / 2 = 4.0

(4和4.0的意义不一样,不要混淆)

  • 在 C 语言中,存在整数除法和浮点除法两种不同的除法方式,是为了满足不同的需求和应用场景。
  • 整数除法(整数除以整数)是指将两个整数相除,且结果只保留整数部分,舍弃小数部分。
  • 浮点除法(两个数中有一个浮点数即可)是指将两个数相除,且结果将包含小数部分。
  • 这两种除法方式的选择取决于需要的精度。如果只关心整数部分并希望结果是一个整数,那么可以使用整数除法。而如果需要精确到小数部分的结果,那么就需要使用浮点除法。

10.1.3: 取模操作符 %

取模操作符又叫做取余操作符,只能适用于整数(包括正整数和负整数),如果又小数则会报错, %就是取出余数作为结果,

10 % 3 = 1

10 % 2 = 2

10 % 1 = 0

需要注意的是,如果对n进行取模,那么n的取值范围在0 ~ (n-1),多利用好这个性质,对后面编程很有帮助

10.2:赋值操作符

10.2.1: 赋值操作符 =

在C语言中,= 操作符用于将一个值赋给一个变量。它将右侧的表达式的结果赋值给左侧的变量。比如:

int num = 10;   // 定义一个整数变量num,并将值10赋给它
 int ret = num   //将num的值赋给ret
  • 等号操作符的左侧必须是一个可修改的变量,而右侧可以是任何合法的表达式。
  • 等号操作符的赋值过程是从右到左的。也就是说,先计算右侧表达式的值,然后将结果赋给左侧的变量。
  • 等号操作符执行的是赋值操作,不是数学运算中的等于。
  • 在连续赋值语句中,等号操作符的结合性是从右到左的。例如,a = b = c将会先将c赋值给b,然后将b的值赋值给a。
  • 等号操作符的返回值是被赋值的变量的值。也就是说,赋值表达式本身的值是被赋值的变量的值。

对于最后一点可能有同学很难理解,这里举一个简单的代码例子示例说明

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int main()
{
  int a = 5;
  int b = (a = 10);  // 将 10 赋值给 a,赋值表达式的值是 10,然后将 10 赋值给 b
  printf("%d\n", a); // 输出 10
  printf("%d\n", b); // 输出 10
  return 0;
}

10.2.2 : += 操作符

int a = 10;

a = a +18;

这段代码很好理解,首先是定义了一个int 类型的变量a 并将10赋值给了a,接着第二行代码表示先将a加上10,再将a +10这个值赋给 a,也等价于 a = 10+18 即 将28赋给a,所以a 等于28

a = a +18有一种缩写形式,即 a += 18;

在编程中,a = a + 18和a += 18是等价的,两者都会将变量a的值加上18,并将结果赋值给a。这两种写法的意义在于简化代码和提高可读性。

使用+=操作符可以使代码更加简洁和易于理解。它是一种常见的缩写形式,可以将加法运算和赋值操作合并到一起。比如,如果你需要多次对一个变量进行累加操作,使用+=可以让代码变得更简洁,方便开发人员编写更简洁的代码。

所以你可以将 a += 18理解为,在a的基础上,再加上18,这样会更方便理解

10.2.2 : -= 操作符

同上,a = a -10 可以缩写成 a -= 10,可以理解为在a的基础上再减10

10.2.2 : *= 操作符

同理,a = a * 9 可以缩写成 a *= 9,可以理解为在a的基础上,再乘上9

10.2.2 : /= 操作符

同理, a = a / 6 可以缩写成 a /= 6,可以理解为在a的基础上 /6

10.3:单目操作符

10.3.1: !操作符

在c语言中,0表示假,而非0的值表示真,举个例子:

0是假

1是真

2是真

-1是真

1.2是真

-1.2是真

而 ! 操作符表示逻辑反操作,就是真变成假,假变成真,

! 0 是真

!1是假

!2是假

!(-1)是假

!1.2是假

!(-1.2)是假

这就是 !操作符的作用

10.3.2: &操作符

&它用于获取一个变量的内存地址,这个地址表示了变量在计算机内存中存储的位置,而在C语言中,地址是用于标识内存中某个特定位置的值的位置。每个变量和每个数据在内存中都有唯一的地址。你可以将地址视为内存中的房间号,而变量和数据就是存放在这些房间中的内容。

比如:

int a = 5;

double b = 1.25;

char c = ‘w’;

对于每个变量来说,变量不仅有对应的值,还要唯一对应的空间,值属性和址属性是一个变量最基本的两个属性,可以将地址视为内存中的房间号,而变量和数据就是存放在这些房间中的内容。在此不过多深入

10.3.3: sizeof操作符

需要注意的是sizeof是一个操作符,而不是函数,许多人误以为sizeof是一个函数,sizeof操作符用于获取变量或数据类型的大小(以字节为单位),下面举一个简单的例子

#include <stdio.h>
int main()
{
  int a = 10;
  int c = sizeof (a);
  printf("a 占用的内存大小是(单位:字节) %d", c);
  return 0;
}

程序的运行结果如下:

这段代码中,首先定义了一个整型变量,接着通过sizeof操作符求出a的大小(以字节为单位),赋给了c,接着打印c的值便可知道 a 所占的内存大小

sizeof的基本用法就是将要求所占空间大小的变量或者类型放在括号内即可

int a = 18;

要求 a 所占空间大小可以用sizeof

下面是sizeof的多种写法

  • sizeof(a)
  • sizeof a
  • sizeof (int)

注意! sizeof int 是错误的写法,要避免,著有当括号内是变量时,括号才能够省略

10.3.4: ++操作符

在C语言中,++操作符用于递增变量的值。每次递增 1 ,它有两种形式:前缀形式和后缀形式

  1. 前缀形式:++var 前缀形式会先将变量的值加1,然后返回递增后的值。例如:
#include <stdio.h>
int main()
{
  int x = 5;
  int y = ++x;// 现在x的值为6,y的值也为6
  printf("x = %d  y = %d", x, y);
  return 0;
}

运行结果如图所示

我来解释一下这两条语句:

  • 首先我们定义了一个整型变量x,x的值为5
  • 在第二行代码中,++在x的前面,故称作前置++,前置++会先自增 1 ,再进行运算
  • 所以第行代码就等价与 int y = 6;所以x和y的值都为6

注意int y = ++x;与int y = x +1并不等价,int y = ++x;不仅改变了y的值,还改变了x的值,让x自增了 1 ,而int y = x +1只是改变了y的值,x的值并未发生改变,请注意区分

  1. 后缀形式:var++ 后缀形式会先使用变量的当前值,然后再将其加1。例如:
  2. `#include <stdio.h>
#include <stdio.h>
int main()
{
  int x = 5;
  int y = x++;// 现在x的值为6,y的值为5
  printf("x = %d  y = %d", x, y);
  return 0;
}

运行结果如图所示

接着我来解释一下这两条语句:

  1. 首先我们定义了一个变量x,x的值为5
  2. 接着因为++是在x的后面,即x++,这是后置++,后置++会先进行运算,运算完后再自增 1
  3. 因为在int y = x++;中,此时x的值为5,我们先进行运算,再对x自增 1
  4. 所以此时 y 的值就是5,运算完后,x的值自增1,x变为了 6

10.3.5:- -操作符

与++操作符类似,- -操作符也有两种前置 - -和后置 - -两种形式

  1. 前缀形式:–var 前缀形式会先将变量的值减1,然后返回递减后的值。例如:
#include <stdio.h>
int main()
{
  int x = 5;
  int y = --x;// 现在x的值为4,y的值也为4
  printf("x = %d  y = %d", x, y);
  return 0;
}

运行结果如图所示

和++操作符类似,因为 - -在x的前面,是前置 - -,我们先自减 1 ,在进行运算

  1. 首先我们定义了一个变量x,x的值为5
  2. 接着因为 - -在x的前面,是前置 - -,我们先自减 1,再进行运算
  3. int y = --x;因为此时x的值是5,x自减后变为了4,并将4 赋值给了y
  4. 所以最后x和y的值都是4

2.后缀形式:var–

后缀形式会先使用变量的当前值,然后再将其减1。例如:

#include <stdio.h>
int main()
{
  int x = 5;
  int y = x--;// 现在x的值为4,y的值为5
  printf("x = %d  y = %d", x, y);
  return 0;
}

运行结果如图所示

  • 首先我们定义了一个变量x,x的值为5
  • 接着因为 - -在x的后面,是后置 - -,先进行运算再自减1
  • int y = x–;,因为此时x的值为5,所以x先进行运算,将5赋给了y,接着x再自减一
  • 所以此时x的值为4,y的值为5

10.3.6: ( 类型)强制类型转换操作符

C语言中的强制类型转换操作符用于将一个数据类型转换为另一个数据类型。它的语法形式是:

(type) expression

  • type表示要转换的目标数据类型
  • expression表示要转换的表达式或变量。

请注意,强制类型转换可能会丢失精度或导致未定义的行为,因此在使用时需要谨慎。应该确保转换的结果是合理和可预期的。

( 类型)操作符是c语言中的强制类型转换操作符,请不要将强制类型转换操作符与函数中的()混淆,比如Add()

下面举一个简单的代码例子示例强制类型转换操作符的作用:

#include <stdio.h>
int main() {
    float num1 = 3.14;
    int num2 = 2;
    int result;
    result = (int)num1 + num2; // 强制将浮点数 num1 转换为整数
    printf("Result: %d\n", result);
    return 0;
}

在上面的代码中,我们定义了一个浮点数变量 num1,一个整数变量 num2,以及一个整数变量 result。我们将 num1 使用强制类型转换操作符 (int) 转换为整数,然后与 num2 相加赋值给 result。最后,我们打印出 result 的值。

输出结果将为 Result: 5,因为 3.14 被转换为整数 3,然后与整数 2 相加得到结果 5。

10.4:关系操作符

C语言中的关系操作符用于比较两个值的大小关系,并返回一个布尔值(0或1)。以下是C语言中常用的关系操作符:

  • 相等(==):判断两个操作数是否相等,如果相等则返回1,否则返回0。
  • 不相等(!=):判断两个操作数是否不相等,如果不相等则返回1,否则返回0。
  • 大于(>):判断左操作数是否大于右操作数,如果是则返回1,否则返回0。
  • 小于(<):判断左操作数是否小于右操作数,如果是则返回1,否则返回0。
  • 大于等于(>=):判断左操作数是否大于等于右操作数,如果是则返回1,否则返回0。
  • 小于等于(<=):判断左操作数是否小于等于右操作数,如果是则返回1,否则返回0。

注意,在c语言中相等符号是 = =,而 = 是赋值符号,请不要混淆赋值符号和相等符号

下面演示一下关系操作符的用处:

#include <stdio.h>
int main() {
    int a = 5;
    int b = 10;
    if (a == b) 
    {
        printf("a equals b\n");
    }
    else 
    {
        printf("a is not equal to b\n");
    }
    if (a > b) 
    {
        printf("a is greater than b\n");
    }
    else 
    {
        printf("a is less than b\n");
    }
    return 0;
}

输出结果是:

a is not equal to b

a is less than b

10.4:逻辑操作符

C语言中的逻辑操作符用于对布尔表达式进行运算,并返回一个布尔值(0或1)。以下是C语言中常用的逻辑操作符:

  • 逻辑与(&&):当两个操作数都为真(非零)时,返回真(1),否则返回假(0)。
  • 逻辑或(||):当两个操作数中至少有一个为真(非零)时,返回真(1),否则返回假(0)。
  • 逻辑非(!):对操作数的逻辑值取反,如果操作数为真(非零),返回假(0),否则返回真(1)。

逻辑操作符常用于条件判断和控制流程中。例如,在if语句中,可以使用逻辑与和逻辑非操作符组合对多个条件进行判断。

以下是一个示例代码,演示了逻辑操作符的使用:

#include <stdio.h>
int main() {
    int x = 10;
    int y = 18;
    int z = -26;
    if (x > 0 && y > 0)
    {
        printf("x和y都是正数\n");//这句话被执行了
    }
    if (x > 0 && z > 0)
    {
        printf("x和z都是正数\n");
    }
    if (x > 0 || z > 0)//这句话被执行了
    {
        printf("x和z中存在有一个是正数\n");
    }
    if (x > 20 || z > 20)
    {
        printf("x和z中存在有一个数大于20\n");
    }
    if (x == y)
    {
        printf("x和y相等\n");
    }
    if (!(x == y))//这句话被执行了
    {
        printf("x和y不相等\n");
    }
    return 0;
}

输出结果为:

我们可以将逻辑与&&理解成我们生活中的并且

将逻辑或 | | 理解成我们生活中的或者

  • &&只有当并且两边的条件都满足了,这个条件才被满足了
  • | |只要有一边的条件满足了,这个条件就被满足了
  • 而 !就是对结果取反,真变成假,假变成真

10.5:条件操作符

在C语言中,条件操作符(也被称为三元运算符)被用于根据一个条件的结果来选择两个值中的一个。它的语法形式为:

condition ? value1 : value2

  • “condition” 是一个条件表达式,它的结果应为真或假
  • 如果条件为真,不会计算value2的值,只计算value1的值,并且整个表达式的结果将为 “value1”
  • 如果条件为假,则不会计算value1的值,只计算value2的值结果为 “value2”。

我们可以通过条件操作符来实现一个简单的双分支,下面通过代码和画图的形式帮助读者理解:

#include <stdio.h>
int main() {
    int a = 88;
    int b = 100;
    int c = (a > b) ? a : b;
    printf("a和b中的较大值是%d",c);
    return 0;
}

运行结果如图所示

流程图如图所示:

我们可以通过条件操作符实现一个双分支,通过这个特性我们可以很快求出a和b中的最大值,并赋给c,最后打印出来

10.4.1: 条件操作符的意义

  • 简化代码:条件操作符可以用来简化代码逻辑,特别是对于简单的条件判断。相比使用 if-else
    语句,条件操作符能够在一行代码中完成相同的判断和赋值操作。
  • 表达式的结果:条件操作符的结果本身也是一个表达式。这意味着可以直接将其作为另一个表达式的一部分使用,而无需引入额外的中间变量。
  • 返回值赋值:条件操作符的结果可以直接赋值给一个变量。这样的赋值语句可以简化代码,并且可以将条件判断和赋值同时完成。
  • 简单选择:当需要从两个值中选择一个时,条件操作符提供了一种简明的方式。例如,在编写一些简单的比较操作时,使用条件操作符可以使代码更加清晰和易读。

总的来说,条件操作符在C语言中的意义在于简化代码、提高可读性和灵活性。它是一种轻量级的条件选择和赋值工具,可以用于简单的条件判断和赋值操作。

10.6:逗号表达式

在C语言中,逗号表达式是一种特殊的表达式,它允许在一个语句中同时使用多个表达式,并以逗号分隔。逗号表达式的结果是最后一个表达式的值。

逗号表达式的语法如下:

(expr1, expr2, expr3, …, exprn)

  • 逗号表达式的执行过程是从左到右,依次计算每个表达式
  • 并且整个逗号表达式的结果为最后一个表达式的值。

下面举个简单的代码例子演示逗号表达式的使用:

#include <stdio.h>
int main() {
    int a = 10;
    int b = 18;
    int c = 22;
    int d = (a + b, b - c, c + a);
    printf("d的值是:%d", d);
    return 0;
}

这段代码是输出结果是:

int d = (a + b, b - c, c + a);在这段代码中,

  • 先计算a + b的值,接着计算 b + c的值,接着计算 c + a的值
  • 并将c + a的值作为逗号表达式整体的值,赋给d

虽然这段代码中直接计算c + a也等于32,那我们是不是遇见逗号表达式就直接计算最后一个式子就可以了呢?答案是否定的,接下来再看一个代码例子:

#include <stdio.h>
int main() {
    int x = 1, y = 2, z = 3;
    int result = (x++, y++, z++, x + y + z);
    printf("result的值是:%d\n", result);
    printf("x的值是:%d\n", x);
    printf("y的值是:%d\n", y);
    printf("z的值是:%d\n", z);
    return 0;
}

该代码会输出:

result的值是:9

x的值是:2

y的值是:3

z的值是:4

如果直接计算最后一个表达式,逗号表达式的结果应该是6,这说明了逗号表达式的每个操作都被顺序执行,并且返回最后一个表达式的结果。所以在实际编程中,不能直接计算逗号表达式的最后一个结果作为逗号表达式的结果

十一: typedef重命名

在C语言中,typedef关键字用于创建类型别名。它可以帮助我们简化复杂的类型声明,并增加代码的可读性。

typedef的基本语法如下:

typedef 原类型 新类型名;

下面是一些typedef的使用示例:

#include <stdio.h>
int main() {
    typedef int Age;//创建自定义类型别名
    Age myAge = 25;
    printf("%d", sizeof(Age));
    return 0;
}

这段程序的输出结果是 4

  • 在这个示例中,我们对int类型进行重命名,创建了一个名为Age的类型别名,它代表了int类型。
  • 然后我们使用该别名来声明一个变量myAge,并将其初始化为25。
  • 接着我们通过sizeof()来查看它的大小,很明显,Age类型的大小和int类型的大小一样

typedef 在 C 语言中具有重要的意义,它允许我们为已有数据类型创建新的别名。这在以下几个方面非常有用:

  • 简化复杂的数据类型:typedef 可以用于给复杂的数据类型创建简短、易于理解的别名,使代码更易读和维护。
  • 代码的可移植性:通过 typedef,我们可以使用具有语义含义的类型名称来表示不同平台或编译器上的相同数据类型,以提高代码的可移植性。
  • 提高代码可读性:通过给类型起有意义的名称,我们可以增加代码的可读性,使其更易于理解和解释。

总的来说,typedef 具有提高代码可读性、可移植性和模块化的优势,可以使代码更易于理解和维护

十二: static关键字

在c语言中,static关键字有三种用法

  1. static修饰局部变量
  2. static修饰全局变量
  3. static修饰函数

12.1: static修饰局部变量

在C语言中,使用static关键字修饰局部变量具有下面两个作用:

  1. 延长局部变量的生命周期:通常情况下,局部变量在函数执行完毕后就会被销毁。但是,当我们使用static关键字修饰局部变量时,它的生命周期会变得更长,即在整个程序运行期间都有效。
  2. 保留变量的值:普通的局部变量每次函数执行时都会重新初始化,而使用static关键字修饰的局部变量在多次函数调用之间会保留其上一次的值。

下面是一个简单的示例代码,演示了static关键字修饰局部变量的作用:

#include <stdio.h>
void example() {
    static int count = 0; // 使用static关键字修饰局部变量
    int normalCount = 0; // 普通的局部变量
    count++;
    normalCount++;
    printf("Static Count: %d\n", count);
    printf("Normal Count: %d\n", normalCount);
}
int main() {
    example(); // 调用example函数
    example(); // 再次调用example函数
    example(); // 再次调用example函数
    return 0;
}

void表示这个函数没有返回值

在上面的代码中,我们定义了一个名为example的函数。在函数内部,我们使用了static关键字修饰了一个名为count的局部变量,同时也定义了一个普通的局部变量normalCount。

每次调用example函数时,count变量都会自增,并且保留上一次的值。而normalCount变量则会在每次函数调用时重新初始化为0。

当我们运行上述代码时,输出将会是:

  1. Static Count: 1 Normal Count: 1
  2. Static Count: 2 Normal Count: 1
  3. Static Count: 3 Normal Count: 1

可以看到,使用static关键字修饰的count变量在多次函数调用之间保持了其值,而普通局部变量normalCount则每次函数调用时都重新初始化为0。

static修饰局部变量,本质上是将变量的存储空间由栈区转移到静态区

  • 栈区上变量的特点是:进入作用域创建,出作用域销毁(注意,销毁只是把空间还给了操作系统,不是让空间报废,没有用)
  • 静态区上变量的特点:创建好后,直到程序结束才销毁

注意,static修饰局部变量只是改变了变量的生命周期,并没有改变变量的作用域,static只是让变量出了作用域仍然存在,直到程序结束,生命周期才结束

12.2: static修饰全局变量

基本格式是:

static int x;

当在C语言中使用static修饰全局变量时,它会改变该全局变量的链接属性,让全局变量的外部连接属性改成了内部连接属性,限制其作用域只在声明它的源文件内部可见,无法被其他源文件访问。此时,该全局变量的作用类似于一个私有变量,只能在声明它的源文件内部使用。

通俗一点说,就是我们在文件 1 中定义的全局变量被static修饰后,那么在文件 2 中就无法使用这个全局变量,无法使用也就修改不了全局变量的值了

使用static修饰全局变量主要有以下几个作用:

  • 隐藏全局变量:通过将全局变量的作用域限制在单个源文件内部,可以避免与其他源文件中同名全局变量发生冲突,从而避免命名空间污染。
  • 保护变量的访问:由于静态全局变量只能被声明的源文件内部访问,外部无法直接访问,可以增加全局变量的安全性,避免被其他部分非法修改。

总体来说,使用static修饰全局变量可以隐藏、保护全局变量的作用范围,使其更加安全且适用于特定的源文件内部使用。

12.2: static修饰函数

基本格式是:

static void func() {
    // 函数体
}

和修饰全局变量类似,在C语言中,使用static修饰函数可以将函数的作用域限制在当前源文件中,从而隐藏函数,使其无法在其他源文件中访问。

通俗一点说,就是我们在文件 1 中定义的函数被static修饰后,那么在文件 2 中就无法使用这个函数了

和全局变量类似,函数也具有外部链接属性,但是函数被static修饰之后,外部链接属性就变成了内部连接属性,使得函数只能在本文件中使用

当static修饰函数时,它的作用是隐藏函数。这可以有效地避免函数与其他源文件中的同名函数发生冲突,提高代码的可维护性。

12.3: extern的用法

extern的基本格式

  • 对于变量的声明: extern 数据类型 变量名;
  • 对于函数的声明:extern 返回值类型 函数名(参数列表);

在C语言中,extern关键字有以下两个主要用途:

  1. 声明外部全局变量:

extern关键字可以用于在一个源文件中声明在其他源文件中定义的全局变量。这样,在当前源文件中就能够使用该全局变量,而不需要重新定义它。

举个例子,假设我们有两个源文件 file1.c 和 file2.c,其中 file1.c 定义了一个全局变量 int globalVar = 10;。在 file2.c 中可以使用 extern int globalVar; 声明,来引用 file1.c 中定义的全局变量 globalVar。

// file1.c
int globalVar = 10;
// file2.c
extern int globalVar;
int main() {
    printf("%d\n", globalVar); // 输出: 10
    return 0;
}
  1. 声明外部函数:

extern关键字可以用于声明在其他源文件中定义的函数。这样,在当前源文件中就能够使用该函数,而不需要重新定义它。

举个例子,假设我们有两个源文件 file1.c 和 file2.c,其中 file1.c 定义了一个函数 void foo() { … }。在 file2.c 中可以使用 extern void foo(); 声明,来引用 file1.c 中定义的函数 foo()。

// file1.c
void foo() {
    printf("Hello from foo!\n");
}
// file2.c
extern void foo();
int main() {
    foo(); // 输出: Hello from foo!
    return 0;
}

需要注意的是,extern关键字只能用于变量或函数的声明,而不能用于定义。变量或函数的定义应该在其他地方,例如其他源文件中。extern关键字的目的是告诉编译器该变量或函数是在其他地方定义的,并在当前文件中引用它。

总的来说,extern关键字用于引用在其他源文件中定义的全局变量和函数,使得在当前源文件中可以使用它们而无需重新定义。这样可以方便地实现源文件之间的变量和函数的共享和通信。

12.4空间内存分布图

4: 内存空间分布

在C语言中,程序运行时会使用三种不同的内存空间:栈区、堆区和静态区(全局区)。

  1. 栈区
  2. 堆区
  3. 静态区

栈区(Stack):

  • 栈区用于存储局部变量以及函数调用时的参数和返回地址。
  • 栈区的内存分配是自动进行的,变量的生命周期与其所在的函数或代码块的执行周期相对应。
  • 栈区的内存管理是由编译器自动完成的,无需手动分配或释放。
  • 栈区的内存空间有限,一旦超出限制,就会导致栈溢出错误。 堆区(Heap):

堆区:

  • 堆区用于存储动态分配的内存,如使用malloc、calloc、realloc等函数分配的内存。
  • 堆区的内存分配是手动进行的,需要手动申请和释放内存。
  • 堆区的内存可以在程序的任意位置进行分配和释放。
  • 堆区的内存空间相对较大,但是需要开发人员自行管理,否则可能导致内存泄漏或者内存溢出等问题。

静态区(Static):

  • 静态区用于存储全局变量和静态变量。
  • 静态区的内存分配在程序运行之初就完成,直到程序结束才会释放。
  • 全局变量和静态变量在整个程序运行期间都存在,具有静态生命周期。
  • 静态区的内存管理由编译器负责,无需手动分配或释放。

了解即可,后面会详细展开

13.1: #define 定义常量和宏

在C语言中,#define是一个预处理指令,用于定义常量和宏。

  • define定义标识符常量:
    #define MAX 1000
  • define定义宏
    #define ADD(x, y) ((x)+(y))

和标识符常量类似,宏也会被替换,比如

  • #define ADD(x, y) ((x)+(y))

ADD(x,y)就会被替换为((x)+(y)),

注意ADD(x, y) 是由#define定义的宏,而不是函数,请注意区分

下面通过一个简单代码例子来说明宏的作用:

#define ADD(x, y) (x)+(y)
#include <stdio.h>
int main()
{
  int sum = ADD(2, 3);
  printf("sum = %d\n", sum);
  sum = 10*ADD(2, 3);
  printf("sum = %d\n", sum);
  return 0;
}

宏定义是使用#define关键字指定的,宏的名称是ADD,宏的替换体是(x)+(y)。这个宏定义表示在代码中使用ADD(x, y)时,会将其替换为(x)+(y)。

在main函数中,有两个使用了宏的行。

  • 第一行int sum = ADD(2, 3);,

表示将宏ADD中的x替换为2,y替换为3,得到的替换结果是(2)+(3),这个结果为5。然后将计算结果赋值给变量sum。

  • 第二行sum = 10*ADD(2, 3);,

表示将宏ADD中的x替换为2,y替换为3,得到的替换结果是(2)+(3)。

请注意! sum = 10*ADD(2, 3); 中的ADD(2,3)被替换后 式子变成了10 * (2)+(3)这个结果等于23,请不要误以为是2和3相加后再乘10,宏只是简单的进行替换,并不运算

关于宏的替换规则,当使用ADD(x, y)时,预处理器会将其替换为(x)+(y)。在替换过程中,预处理器会将参数x和y直接替换到宏定义中的对应位置。这种替换是简单的文本替换,没有类型检查或计算。因此,在使用宏时要确保参数和替换体的正确匹配,并注意避免出现意料之外的错误。

十三: 指针

  • 内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。
  • 所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。
  • 为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地 址。

一一个小格为一个单位,一个字节就是8个小格

因为一个二进制位可以存放0和1,有两种组合方式,一个字节有8位,所以一个字节可以有28= 256 中组合方式

在每一个字节的起始位置处,都有一个地址,我们通过这个地址就可以找到这个空间在内存中的位置

注意,每个地址都是唯一的

13.1: 指针和地址的大小

目前计算机主要分为32位计算机和64位计算机两种,

  • 在32位计算机中,指针的大小为4字节,
  • 在64位计算机中,指针的大小为8字节

指针是用于存放地址的变量

在计算机上,有地址线电线产生的高低电平信号,这些信号会转换成数字信号0 和 1,32位机器顾名思义就是有32根地址线,而64位机器就是有64根地址线

对于一根地址线,它有两种组合,即0和1

对于两根地址线,它有4种组合,即00 01 10 11

对于三根地址线,它有8种组合,即 000 001 010 011 100 101 110 111

……

以此类推

那么32根地址线,就有232种组合,它能够管理232个地址

64根地址线,就有264种组合,它能够管理264个地址

所以我们要想储存一个变量的地址,

  • 在32位机器种只需要32个二进制位
  • 在64位机器种只需要64个二进制位即可

而一个字节又等于8个比特位,所以

  • 在32位机器中,指针的大小为4字节
  • 在64位机器中,指针的大小为8字节

注意,指针和地址是两个不同的概念,指针是用于存放地址的变量,而地址是变量中的很重要的一个属性

13.2: 指针的使用

那么我们该如何通过指针来存放一个变量的地址呢?

#include <stdio.h>
int main()
{
  int num = 10;
  int* p = &num;
  *p = 20;
  printf("%d", num);
  return 0;
}

运行结果如图所示

接下来我将通过画图的形式讲解

10的二进制是1010,如果补齐64位的话,就是

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00001010

这就是10在内存中存储的数据

如图所示,我们定义了一个数值为10的空间,接着我们通过取地址符&,将num的地址取出来,注意,取地址取出来的是开头处的地址,然后把地址赋给了专门存放地址的变量->指针int *p,此时p就存放了num开头的地址了

那么此时我们就有了num的地址,并将地址存放在指针变量p中,我们该如何通过p来找到num呢?

这时又另外一种操作符

解引用操作符 *

注意,这个解引用操作符和p上的*意义完全不同,不要混淆

解引用操作符就是取地址操作符的逆运算

&num 就是通过num找到num的地址

而*p 则是通过num的地址,找到num(此时p存放的是num的地址)

我们说过,一个变量它有值属性和址属性两种属性

那么,这两个属性有什么关联吗?

答案是有的,我们可以通过值属性找到址属性,也可以通过址属性找到值属性

那么我们再来解释一下上面的代码

首先我们定义了一个int 类型的num变量,并赋值为10,接着将num的地址取出来,并将地址赋给了指针p,接着我们通过解引用操作符 * 来找到num的值,并将20赋给它,所以num的值也就被改成了20

十四: 结构体

在生活中,基本数据类型可以描述绝大多数的物体,比如说名字,身高,体重,但是还有一部分物体还不足够被描述,比如说我们该如何完整的描述一本书呢?包括书的名字,价格,作者。页数,等等,由此便有了结构体这个概念

C语言中的结构体是一种自定义的数据类型,它允许我们将不同类型的数据组合在一起,形成一个逻辑上相关的数据单元。结构体可以包含不同的数据类型,如整型、字符型、浮点型、指针等,并且可以根据需要添加多个成员变量。

结构体的定义使用关键字struct,后面跟着结构体的名称和花括号。在花括号中定义结构体的成员变量,每个成员变量由数据类型和名称组成,中间用分号分隔。

例如,下面是一个定义了一个简单的学生结构体的例子:

struct Student {
    int id;
    char name[20];
    float score;
};

其中Student是结构体的名字,int id; char name[20]; float score;是结构体的成员

14.1: 结构体的赋值

那么我们该怎么对结构体赋值呢?

struct Stu s = {"张三", 20, "男", "20180101"};

这样顺序赋值即可,后面会深入讲解结构体,目前了解这种方式即可

14.1: 结构体成员的访问

结构体成员的访问有两种方式

  • 第一是通过 . 操作符
  • 第二是通过 ->操作符

下面是代码示例:

#include <stdio.h>
struct Stu
{
  char name[20];
  int age;
  char sex[5]; 
  char id[15];
};
int main()
{
  struct Stu s = { "张三", 20, "男", "20180101" };
  printf("name = %s age = %d sex = %s id = %s\n", s.name, s.age, s.sex, s.id);
  struct Stu* ps = &s;
  printf("name = %s age = %d sex = %s id = %s\n", ps->name, ps->age, ps->sex, ps -> id);
  return 0;
}

运行结果为

本章完,后续会陆续更新c语言的知识,如果本文中出现错误,欢迎指出!!

目录
相关文章
|
2月前
|
安全 编译器 C语言
C++入门1——从C语言到C++的过渡
C++入门1——从C语言到C++的过渡
72 2
|
26天前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
81 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
2月前
|
存储 Java 编译器
初识C语言1——C语言入门介绍
初识C语言1——C语言入门介绍
37 1
|
2月前
|
C语言
回溯入门题,数据所有排列方式(c语言)
回溯入门题,数据所有排列方式(c语言)
|
4月前
|
C语言
C语言------程设设计入门
这篇文章是C语言程序设计的入门教程,涵盖了C程序的实现过程、VC集成开发环境的使用、基本数据类型的使用、格式控制字符的作用,以及通过示例代码演示了如何使用printf()函数输出不同类型的数据。
C语言------程设设计入门
|
5月前
|
存储 Java C语言
【C语言入门】初识C语言:掌握编程的基石
【C语言入门】初识C语言:掌握编程的基石
78 4
【C语言入门】初识C语言:掌握编程的基石
|
4月前
|
NoSQL Java 编译器
C语言从入门到精通该怎样学?
持续学习与实践:编程是一门需要不断学习和实践的技能,要保持对新技术和新知识的敏感性,并持续进行编程实践。
67 1
|
5月前
|
存储 Java 程序员
【C语言入门】C语言入门:探索编程世界的基础概念
【C语言入门】C语言入门:探索编程世界的基础概念
128 2
|
5月前
|
前端开发 C语言 C++
C语言入门02---环境搭建
C语言入门02---环境搭建
|
6月前
|
存储 Web App开发 算法
c语言的简单入门
熟悉c语言(简单入门)