【C语言】与C语言的初次见面,请多多关照哦 ~

C语言学习之路 专栏收录该内容
5 篇文章 1 订阅

阅前说明

C语言学习之路系列博客,是博主自己在学习C语言的过程中所做的笔记,把知识框架整理记录下来,为了后续回顾与复习,同时也希望该博客可以帮助到一些正在学习C语言的小伙伴。博客内容如有错误或疏漏,请大家指出,谢谢啦,博主一定多多学习,及时改正过来。

使用的编译器:Visual Studio 2019(下载地址


本篇前言

本篇大致介绍了C语言的基础知识,希望能让大家对C语言有一个大概的认识。每个知识点只是简单介绍,后续篇章都会一一详细讲解。


点击可直接跳转

(1)什么是C语言?

C语言广泛应用于底层开发(不做详细介绍,可百度)

image-20210424180255962
  • 计算机语言发展过程

    • 二进制语言 0/1
    • 汇编语言 - 助记符 ADD/AND
    • 高级语言

第一个C语言程序

//test.c(testcode project)
#include <stdio.h>
int main()
{
  printf("hello C language\n");
  return 0;
}

main函数是程序的入口

一个工程/项目可以有多个.c文件,但只能有一个main函数


(2)数据类型

char     //字符数据类型 
short    //短整型 
int     //整型
long     //长整型 
long long  //更长的整型 
float    //单精度浮点数 
double    //双精度浮点数
  • C语言中没有没有字符串类型?

    没有

  • 为什么会出现这么多类型?

    更加丰富的表达生活中的各种值,可以提高空间利用率

  • 计算机中的数据以二进制(补码)形式存储在内存中

    原文地址:CPU寻址范围(寻址空间)一系列问题

每种类型的大小是多少?

这里需要用到 sizeof() 操作符 - 获取类型或者变量所占空间大小(以字节为单位)

#include<stdio.h>
int main()
{
	//每种类型的大小是多少?
	printf("%d字节\n", sizeof(int));
	printf("%d字节\n", sizeof(short));
	printf("%d字节\n", sizeof(long));
	printf("%d字节\n", sizeof(long long));
	printf("%d字节\n", sizeof(char));
	printf("%d字节\n", sizeof(float));
	printf("%d字节\n", sizeof(double));
	printf("%d字节\n", sizeof(long double));
	return 0;
}

运行结果(x86平台):

C语言标准:规定只要sizeof(long)>=sizeof(int)就可以了,不需要一定大于

计算机中的单位

bit - 比特位 - 一个比特位存放一个二进制位 0/1

byte - 字节 - 一个字节 = 8bit

kb - 1kb = 1024byte

mb - 1mb = 1024kb

计算机中的进制

二进制:0 1

八进制:0 ~ 7(三位二进制等于一位八进制)

十进制:0 ~ 9

十六进制:0 ~ 9 , A ~ F(四位二进制等于一位十六进制)


(3)变量、常量

变量的定义

注:建议定义(局部)变量时给它初始化一个值,避免产生随机值造成错乱

#include<stdio.h>
int main()
{
	int age = 0;
	/*
    给float类型变量赋值,通常在数值后面加上f,表示单精度浮
    点数,如果不加f,系统会把赋值的数值当作double类型处理,然
    后再把这个double类型的值赋值给float类型,这样会出现精度丢失。
    */
    float price = 10.5f;
	double weight = 52.3;
	return 0;
}

变量的分类

  • 局部变量
  • 全局变量(不提倡使用,后续讲原因)
#include<stdio.h>
int a = 100;  //全局变量 - { }外部定义的
int main()
{
	int a = 10;  //局部变量 - { }内部定义的
	printf("a = %d", a);  //输出a = 10
	return 0;
}

思考:为什么会输出 a = 10 呢?

当局部变量和全局变量名称冲突时,局部优先

不建议把全局变量和局部变量的名字写成一样(如上面代码中的 a)


变量的作用域和生命周期

变量的作用域

这个变量可以在哪里使用,哪里就是它的作用域

  • 局部变量的作用域

    变量所在的局部范围

#include<stdio.h>
int main()
{
	int a = 10;
	printf("%d\n", a);
    
	{
		int b = 20;  //b的作用域为此 {} 内
	}
	//printf("%d\n", b);  //error: "b"是未声明的标识符
	return 0;
}
  • 全局变量的作用域

    整个工程(在同一工程的其它文件下使用需要用 extern 声明一下变量)

/*源文件demo2.c(test_4_5 project)*/

int x=5;
/*源文件demo.c(test_4_5 project)*/

#include<stdio.h>
int main()
{
    //声明 demo2.c 文件中的全局变量
    extern int x;
    
    printf("%d\n", x);
    return 0;
}

变量的生命周期

变量的创建到销毁之间的时间段

//局部变量的生命周期:进入局部范围生命开始,出局部范围生命结束
//全局变量的生命周期:就是整个程序的生命周期

#include<stdio.h>
int main()  //一个程序的生命周期就是 main 函数的生命周期
{
	{  //变量 a 声明开始
		int a = 10;
		printf("%d\n", a);
	}  //变量 a 声明结束
	return 0;
}

常量

C语言中常量分为以下几种:

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

1、字面常量

//字面常量
3.14;
10;
'a';  //字符常量 - 由一对单引号引起来的单个字符
"abcdef";  //字符串常量

2、const修饰的常变量

常变量 - 具有常属性(不能被改变的属性) - 本质上还是变量,不能当常量使用

这样是错误的(只有在C99语法下才支持变长数组)

image-20210406174622468

这样是可以的

int arr[10] = { 0 };
const int N = 9;
//思考:这两种写法正确吗?
arr[N] = 10;  //这样是可以的
N = 20;  //error

3、#define 定义的标识符常量

使用 #define 指令为程序中的常量赋予有意义的名称,它只是一个符号,在预编译阶段进行字符替换

(如下:所有 MAX 符号将被替换成 100)

#include<stdio.h>
#define MAX 100  //#define 定义的标识符常量
int main()
{
    MAX = 200;  //error
    return 0;
}

4、枚举常量

枚举常量 - 可以一一列举的常量:比如星期、三原色、性别

//定义枚举常量Sex
enum Sex
{
	//枚举常量默认的值: 0 1 2 3 …… 依次递增 - 其值不可被更改
	Male,    // 0
	Female,  // 1
	Secret   // 2
};

#include<stdio.h>
int main()
{
	enum Sex S1 = Male;
	printf("%d\n", Male);  	 //输出:0
	printf("%d\n", Female);  //输出:1
	printf("%d\n", Secret);  //输出:2
	return 0;
}

枚举常量不可以赋值,但可以在定义的时候 指定值,这个可以认为是定义值,而不是赋值。

enum Sex
{
	//这个可不是赋值哦
	Male = 3,
	Female = 100,
	Secret
};
printf("%d\n", Male);
printf("%d\n", Female);
printf("%d\n", Secret);

打印其值,运行结果为:
3
100
101


(4)字符串、转义字符、注释

字符串

1、什么是字符串:

字符串就是由双引号引起来的一串字符

"hello world"

2、字符串的存储:

因为C语言没有字符串这种数据类型,所以要用到「 字符数组 」来存储字符串(每个元素都是字符类型的数组)

#include<stdio.h>
int main()
{
	char arr[] = "hello";
    //['h']['e']['l']['l']['o']['\0']
	return 0;
}

3、字符串的结束标志:

C语言规定,在每一个字符串常量的结尾,系统都会自动加一个字符 ‘\0’ 作为该字符串的“结束标志符”,系统据此判断字符串是否结束。计算字符串长度时,’\0’ 不算作字符串的内容哦;但是计算数组长度(或元素个数或所占内存空间大小),’\0’ 要算进去。

通过调试,打开监视窗口可看到字符数组内部情况

image-20210414194135679 image-20210414193943566
  • 接下来我们用代码来验证一下:
#include<stdio.h>
int main()
{
	char arr1[] = "abc";  //字符串
	char arr2[] = { 'a','b','c' };  //是字符数组,但不代表字符串,末尾没有'\0'
	
	printf("%s\n", arr1);
	printf("%s\n", arr2);
	return 0;
}

调试这段代码,我们发现,arr1 数组尾部有结束标志 ‘\0’ ,而 arr2 数组后面却没有 ‘\0’

image-20210414195943309

再来看看运行结果,arr1 正常输出,而 arr2 后面却乱码了,输出了一堆奇怪的符号,这是为什么呢?

因为在输出 arr1 中的内容过程中,遇到了 ‘\0’ 是字符串的结束标志,所以停止输出;

而输出 arr2 中的内容时,输出完 abc 没有遇到字符串结束标志 ‘\0’ ,不知道什么时候停下来,而后面的内存空间的内容是未知的,放的是啥我们不知道,所以输出乱码。

![](https://img-blog.csdnimg.cn/img_convert/3984e84c9a5a3f9e3c9507928fe6bd98.png style="zoom:80%)

现在我们修改代码,主动在 arr2 后面放上 ‘\0’ ,输出看看效果

#include<stdio.h>
int main()
{
    //定义并初始化字符串的两种方式
    //一定不要忘记结束标志 '\0' 了哦
	char arr1[] = "abc";
	char arr2[] = { 'a','b','c','\0' };
    
	printf("%s\n", arr1);
	printf("%s\n", arr2);
	return 0;
}

运行结果:都能打印出 abc 由此可见,字符串的结束标志是至关重要的

image-20210414202940562

4、获取字符串的长度:

这里要使用到一个函数 strlen() - string length - 调用该函数需要引用头文件<string.h>

#include<stdio.h>
#include<string.h>  //调用strlen()函数
int main()
{
	char arr1[] = "abc";
	char arr2[] = { 'a','b','c' };
	char arr3[] = { 'a','b','c','\0' };
	//打印字符串的长度
	printf("%d\n", strlen(arr1));
	printf("%d\n", strlen(arr2));
	printf("%d\n", strlen(arr3));
	return 0;
}

运行结果:获取字符串的长度并输出时,字符数组 arr2 因为末尾没有字符串结束标志 ‘\0’ ,不知道长度到底是多少,所以输出随机值,获取不了字符串的长度。

注:在计算字符串长度的时候 ‘\0’ 是结束标志,不算作字符串的内容。

image-20210416210559941

5、计算字符数组/数组的长度:

计算数组长度(或元素个数或所占内存空间大小), ‘\0’ 要算进去

#include<stdio.h>
int main()
{
	char arr[] = "abc";
	int len = 0;
    //计算数组 arr 的长度:数组内存总大小(字节)/单个数组元素内存大小(字节)
	len = sizeof(arr) / sizeof(arr[0]);
	
    printf("len = %d\n", len);
	return 0;
}

运行结果:4(因为字符数组 arr 后面还有一个系统自动添加的结束符 ‘\0’ )

6、一定要区分开 ‘0’ 和 ‘\0’ 和 0

字符 ‘0’ —— ASCII码值为 48

转义字符 ‘\0’ —— 字符串的结束标志(不算作字符串的内容) - ASCII码值为 0

数字 0


转义字符

1、什么是转义字符

转义字符顾名思义就是转变了字符原来的意思

来段代码感受一下,如果我们想要在屏幕上打印一个目录:c:\test\test.c

#include<stdio.h>
int main()
{
	printf("c:\test\test.c");
	return 0;
}

运行结果:很奇怪,和我们想象的不一样唉,没有打印出目录,却出现了两段空白,这个字符\和字符t不再是普通的字符的意思了,转变了原来的意思,变成了转义字符\t(水平制表符)

image-20210415224750303

同理,字符\和字符n也不再是两个普通的字符了,而是转义字符\n(换行符)

image-20210415225644519

2、C语言中有哪些转义字符

转义字符释义
\’用于表示字符常量'
\"用于表示一个字符串内部的双引号"
\\用于表示一个反斜杠,防止它被解释为一个转义序列符
\a警告字符,蜂鸣
\n换行
\r回车
\t水平制表符(键盘上的 Tab 键)
\v垂直制表符
\dddddd表示 1-3 个八进制的数字,如:\130(从 \000 到 \377)
\xdddd表示 2 个十六进制的数字,如:\x30
\b退格符
\f进制符
?在书写连续多个问号时使用,防止他们被解析成三字母词

3、转义字符的应用

  • 思考问题:在屏幕上打印一个单引号'怎么做?

大家再没有学习转义字符之前可能会想直接输出打印就好啦,而写成下面这个错误示范了哦

错误示范:中间的你想要输出的那个单引号 ’ 和左边的单引号组成一对了,编译会 error

printf("%c\n", ''');

修改正确:为了防止其与左边的单引号组成一对,需要在单引号前加一个转义符

printf("%c\n", '\'');  //这里 \'被解析成一个转义字符

同理:在屏幕上打印一个双引号 " 就可以这样写啦

printf("%s\n", "\"");
  • 回到最初的问题:在屏幕上打印一个 c:\test\test.c 怎么做?
printf("c:\\test\\test.c");
  • \ddd 与 \xdd :使用编码值来间接的表示字符
//输出字符 A - 对应的ASCII码的8进制形式是101 - 8进制的101是十进制的65
printf("%c\n", '\101');

//输出字符 a - 对应的ASCII码的8进制形式是141 - 8进制的141是十进制的97
printf("%c\n", '\141');

//输出字符 B - 对应的ASCII码的16进制形式是42
printf("%c\n", '\x42');

//输出字符 b - 对应的ASCII码的16进制形式是62
printf("%c\n", '\x62');

对于转义字符来说,只能使用八进制或者十六进制。

转义字符的初衷是用于 ASCII 编码,所以它的取值范围有限:

  • 八进制形式的转义字符最多后跟三个数字,也即\ddd,最大取值是\177
  • 十六进制形式的转义字符最多后跟两个数字,也即\xdd,最大取值是\x7F

@参考文章:C语言转义字符

4、ASCII编码

补充知识:

计算机只认识 0 和 1 两个数字,数据在内存中以二进制形式存储,我们在屏幕上看到的文字,在存储之前都被转换成了二进制(0和1序列),在显示时也要根据二进制找到对应的字符。

那么,怎么将英文字母和二进制对应起来呢?这就需要有一套规范,一种专门针对英文的字符集 - ASCII编码就被设计出来了,为每个字符分配了唯一的编码值在C语言中,一个字符除了可以用它的实体表示,还可以用编码值表示。使用编码值来间接地表示字符的方式称为转义字符。

在 ASCII 编码中,大写字母、小写字母和阿拉伯数字都是连续分布的(见下表),这给程序设计带来了很大的方便。例如要判断一个字符是否是大写字母,就可以判断该字符的 ASCII 编码值是否在 65~90 的范围内。

@参考文章:ASCII编码,将英文存储到计算机

ASCII码表

此图转载自百度

5、测试题:

#include<stdio.h>
#include<string.h>
int main()
{
	//程序输出什么呢?
	printf("%d\n", strlen("c:\test\328\test.c"));
	return 0;
}

运行结果:14(\t\32分别是一个转义字符)


注释

注释有两种风格:

在编写C语言源代码时,应该多使用注释,这样有助于对代码的理解。

  • C语言风格的注释(缺陷:不能嵌套注释)
/*xxxxxxxxxxxx*/
  • C++风格的注释(可以注释一行也可以注释多行)
//xxxxxxxxxxxx

(5)操作符

这里只是简单介绍一下各种操作符,目的是能够让大家在代码中识别出这些,后面我们会详细讲解和学习

算数操作符

+ - * / %

移位操作符

移位操作符含义
>>右移操作符 - 把二进制位向右移动 - 缺位补 0
<<左移操作符 - 把二进制位向左移动 - 缺位补 0

举例说明:

int a = 1;
int b = a << 1;
//a - 00000000000000000000000000000001 --> a = 1
//b - 00000000000000000000000000000010 --> b = 2

位操作符

位操作符含义
&按位与
|按位或
^按位异或

赋值操作符

=  //赋值符
+=  -=  *=  /=  &=  ^=  |=  >>=  <<=  //复合赋值符

举例说明:

int a = 0;
a = a + 1;
//等效于: 
a += 1;

单目操作符

单目操作符含义
!逻辑反操作
-负值
+正值
&取地址
sizeof操作数的类型长度(以字节为单位)
~对一个数的二进制序列按位取反
前置、后置–
++前置、后置++
*间接访问操作符(解引用操作符)
(类型)强制类型转换

注:

双目操作符:有两个操作数(比如:a + b)

单目操作符:只有一个操作数

补充知识:C语言中如何表示真和假?

0 表示假,非 0 表示真

下面来展开讲解几个操作符

  1. 按(二进制)位取反操作符:~

对一个数的所有二进制位取反,0 变成 1,1 变成 0

#include<stdio.h>
int main()
{
	int a = 0;
	int b = ~a;  //按位取反
	printf("%d\n", b);  //输出的是 b 的原码
	return 0;
}

运行结果:-1

这个时候大家可能会有疑问了,为啥会是 -1 呀,别着急,仔细听我道来

这里需要引入原码 反码 补码的概念

整数在内存中存储的都是二进制形式的「 补码 」,输出打印时 要将其转换成「 原码 」

「 原码 」第一位是「 符号位 」表示 「 正负 」(1为负,0为正)

所以:

a - 00000000000000000000000000000000(补码)

对 a 的二进制位( ~ 按位取反)得到 b 的二进制形式的「 补码 」,因为输出的是「 原码 」所以我们进一步转换:

b - 11111111111111111111111111111111(补码)

b - 11111111111111111111111111111110(补码 -1得到反码)

b - 10000000000000000000000000000001(符号位不变,其它位按位取反得到原码)

而二进制序列 10000000000000000000000000000001 就是 -1

知识点:

整数(正数、负数)的原码反码补码:

  • 正数:原码 = 反码 = 补码 相同
int i = 2;
00000000000000000000000000000010 - 原码
00000000000000000000000000000010 - 反码
00000000000000000000000000000010 - 补码
  • 负数
二进制形式转换规则
原码按照整数正负 写出二进制序列
反码「 原码 」符号位不变 其它位按位取反
补码反码 + 1
int i = -2;
10000000000000000000000000000010 - 原码
11111111111111111111111111111101 - 反码
11111111111111111111111111111110 - 补码
  1. **自增自减操作符:++ – **
#include<stdio.h>
int main()
{
	int a = 10;
	int b = ++a;  //前置++:先++ 后使用
	int c = a++;  //后置++:先使用 后++

	printf("%d\n", a);
	printf("%d\n", b);
	return 0;
}

前置后置-- 同理

同时建议大家不要去研究这样的代码,浪费时间,在不同的编译器得到的结果都不一样,平时开发也几乎不会这样去写,可读性太差

int a = 1;
int b = (++a) + (++a) + (a++);
  1. 强制类型转换:(类型)

一般在类型不匹配的时候使用,不推荐,既然你会使用强制类型转换,说明一开始在编写程序的时候就没有设计好各变量的数据类型,导致程序有缺陷

int a = (int)3.14;

关系操作符

>
>=
<
<=
!=  用于测试“不相等”
==  用于测试“相等”

逻辑操作符

描述并且、或者的关系

逻辑 与 / 或 的结果为:真 / 假

&&   逻辑与
||   逻辑或

条件操作符

表达式 exp1 成立,整个表达式的结果是 exp2 的结果

表达式 exp1 不成立,整个表达式的结果是 exp3 的结果

exp1 ? exp2 : exp3  //也是三目操作符,有三个操作数

举例说明:

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int max = a > b ? a : b;
	printf("max = %d\n", max);
	return 0;
}

运行结果:max = 20

逗号表达式

逗号隔开的一串表达式,从左向右依次计算的,整个表达式的结果是最后一个表达式的结果

exp1, exp2, exp3, …expN

举例说明:

#include<stdio.h>
int main()
{
	int a = 1;
	int b = 2;
	int c = 5;
    //思考 d 的值是多少?
	int d = (a = b + 2, c = a - 4, b = c + 2);
	printf("d = %d\n", d);
	return 0;
}

运行结果:d = 2

思考题:

函数调用 exec( ( vl, v2 ), ( v3, v4 ), v5, v6 ); 中,实参的个数是:( 4 ) - 分别是v2 v4 v5 v6

下标引用、函数调用、结构成员

[]	下标引用操作符
()	函数调用操作符
.	结构体成员访问操作符
->

举例说明

image-20210418211613332

(6)常见关键字

1.关键字是C语言提供的,不能自己创建关键字
2.关键字不能做变量名

auto(修饰局部变量,一般是省略的)  break   case  char  const   continue  default  
do   double	  else  enum(枚举)  extern(声明外部符号)  float  for   goto  if   int   
long  register(寄存器)   return   short   signed(有符号数)   unsigned(无符号数)  
sizeof(计算机类型/变量所占内存空间大小)   static(静态)   struct(结构体)   switch  
typedef(类型定义)   union(联合体/共用体)   void   volatile(C语言中暂时不讲)   while
  • 补充知识:

计算机中,数据可以存放到哪里呢?

寄存器(会被频繁调用的数据,放在寄存器中,可以提高效率,现在编译器已经能够自动识别这种数据并放在其中,register实用意义不大)

高速缓存

内存

硬盘

  • 思考:#define是不是关键字?include是不是关键字?

都不是,它们是预处理指令(预编译期间处理的指令)

关键字这次先简单介绍几个,后续遇到了再讲解

1、关键字 typedef

类型重定义,类型重命名,相当于取了一个别名

举例说明:

#include<stdio.h>

//把 unsigned int 重命名为 u_int,现在 u_int 也是一个类型名了
typedef unsigned int u_int;
int main()
{
    //a 和 b 的类型相同
	unsigned int a = 10;
	u_int b = 20;
	
    return 0;
}

2、关键字 static

static 用来修饰变量和函数

1)修饰局部变量 - 静态局部变量

2)修饰全局变量 - 静态全局变量

3)修饰函数 - 静态函数

static 修饰局部变量

改变了局部变量的生命周期(本质上是变量的存储类型),让静态局部变量出了作用域依然存在,直到程序结束生命周期才结束

代码1:

#include<stdio.h>
void test()
{
	int a = 1;  //局部变量 a
	a++;
	printf("%d ", a);
}
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		test();
	}
	return 0;
}

运行结果:2 2 2 2 2 2 2 2 2 2(10个2)

代码2:

#include<stdio.h>
void test()
{
	static int a = 1;  //静态局部变量a
	a++;
	printf("%d ", a);
}
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		test();
	}
	return 0;
}

运行结果:2 3 4 5 6 7 8 9 10 11

为啥会出现这样的结果呢?

这里补充一点小知识,内存会被分为几个区域,今天在这里我们只讨论和这次C语言有关的几个区域,局部变量 a 开始放在栈区的,被 static 修饰后就被放到静态区去了,改变了它的存储类型,存在不同的区域,就有不同的特点了,静态变量 a 的生命周期和全局变量一样,为整个程序

image-20210422113845034

static 修饰全局变量

static修饰全局变量,使得这个全局变量只能在自己所在的源文件(.c)内使用

不能在同一工程的其他源文件内使用

代码实例:

/*源文件demo2.c(test project)*/

//静态全局变量g_val
static int g_val = 2021;
/*源文件demo.c(test project)*/

#include<stdio.h>
//声明静态全局变量 g_val
extern int g_val;
int main()
{
	printf("%d\n", g_val);
	return 0;
}

运行结果:error

image-20210422123125561

运行程序,结果error了,那么 static 修饰全局变量的本质是什么呢?

全局变量,可以在同一工程的其他源文件内被使用,是因为全局变量具有外部链接属性

但是当被 static 修饰成静态全局变量,就变成了内部链接属性,其他源文件就不能链接到这个静态全局变量了

所以无法被其他源文件所使用,只能在自己所在的源文件内使用

static 修饰函数

static修饰函数,使得这个函数只能在自己所在的源文件(.c)内使用

不能在同一工程的其他源文件内使用

本质上是:static 将函数的外部链接属性变成了内部链接属性(和 static 修饰全局变量一样)

仔细对比代码1和代码2,理解 static 在修饰函数前后的意义

代码1:

image-20210423112350703

代码2:

image-20210423113111570

(7)#define 定义宏

#define 是C语言提供的宏定义命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。该命令有两种格式:一种是简单的宏定义,另一种是带参数的宏定义。

在该程序被编译前,先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译,宏替换是简单的替换。

还有就是要记住这是简单的替换而已,不要在中间计算结果,一定要替换出表达式之后再算。

//普通宏
#define PI (3.1415926)
//带参数的宏
#define ADD(a,b) ( (a) + (b) )
//关键是十分容易产生错误,包括机器和人理解上的差异等等

来看一个例子:

int a = 2 * ADD(2, 3);

预处理阶段之后,ADD(2, 3) 则会被替换成 ( (2) + (3) )

int a = 2 * ( (2) + (3) );  //结果为 10

如果没有括号

#define ADD(a,b) a + b

替换之后的结果是:

int a = 2 * 2 + 3;  //结果为 7

(8)选择语句

为了先将C语言拉通介绍一遍,能够对其有一个整体的认识,这里的内容只进行一个简单的介绍,后续会详细讲解

生活中的选择无处不在,那么在计算机中,怎么用C程序设计语言表示呢?

如果(if)就……

否则(else)就……

这就是选择!

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	if (a > b)
	{
		printf("较大值:a\n");
	}
	else
	{
		printf("较小值:b\n");
	}
	return 0;
}

如果 a 比 b 大,输出 a,否则就输出 b


(9)循环语句

生活中有些事,要一直做,重复做,比如学习、吃饭、睡觉等等,如何用C语言实现循环呢?

  • while 语句
  • for 语句
  • do…while 语句

后面会详细讲解


(10)函数

假设(我是 main函数)(张三是 add 函数),有一天,我想让张三给我带饭,所以得告诉他我想带什么饭,并给他带饭的钱,(蛋炒饭,10元),张三接收到信息和拿到钱,最后返回(return)给我一份蛋炒饭

实例:用函数实现求两数的和

#include<stdio.h>

int add(int x, int y)  //求两数的和
{
	return x + y;
}

int main()
{
	int a = 10;
	int b = 20;
	int sum = add(a, b);
	printf("sum = %d\n", sum);
	return 0;
}

函数的特点就是简化代码,代码复用。
每个函数都是独立的,函数的定义和实现必须放在程序中所有函数的外面,不能在其它函数内进行定义和实现,函数之间是可以嵌套调用。


(11)数组

我们需要存储 10 个数字,怎么办呢?如果去定义 10 个变量来存储,那也太麻烦了,这时我们可以用数组

C语言中数组的定义:一组相同数据类型的元素的集合

  • 数组的定义与初始化
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };  //完全初始化
int arr2[5] = { 1,2,3 };  //不完全初始化,剩余的默认为 0
char arr3[5] = { 'a','b','c' };  //不完全初始化,剩余的默认为 '\0'

注意一下整型数组 arr2 和字符数组 arr3 的不完全初始化的区别

  • 数组元素的访问(通过数组下标访问,数组的下标是从 0 开始的)
arr1[0] = 1;
arr1[1] = 2;
……
arr1[9] = 10;

(12)指针

指针是C语言中非常非常重要的内容,我们要谈指针,必须先要搞明白内存

1、什么是内存

内存是电脑上特别重要的存储器,计算机中所有程序的运行都是在内存中进行的。

为了有效的使用内存,就把内存划分成一个个小的内存单元,「 每个内存单元的大小是1个字节 」。

为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该「 内存单元的地址 」。

image-20210423144058148
  • 内存单元是如何编号的?

电脑分为 32位平台 和 64位平台 的,以 32位平台 举例

CPU有32根地址线 - 通电会产生 1/0 两种电信号 - 电信号转换成数字信号 - 变成32个 1/0 组成的二进制序列

共有 2 (32) = 4,294,967,296 种不同的序列

0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0001
…………
0111 1111 1111 1111 1111 1111 1111 1111
1111 1111 1111 1111 1111 1111 1111 1111

我们就可以用这些二进制序列来给内存单元编号,1个内存单元(1字节)的编号由32个0/1组成,这就是它们的地址。存储地址一般用十六进制数表示(如上图所示)

内存地址只是一个编号,代表一个内存空间。在计算机中存储器的容量是以字节为基本单位的。也就是说一个内存地址代表一个字节(8bit)的存储空间。例如经常说 32位 的操作系统最多支持4GB的内存空间,也就是说CPU只能寻址2的32次方(4GB)空间,再多的话CPU就找不到了。

补充知识点:CPU寻址范围

  • 寻址空间一般指的是CPU对于内存寻址的能力。通俗地说,就是CPU最多能用到多少内存的一个问题。数据在存储器(RAM)中存放是有规律的
    ,CPU在运算的时候需要把数据提取出来,这就需要知道数据在哪里,这时候就需要挨家挨户的找,这就叫做寻址。但如果地址太多超出了CPU的能力范围,CPU就无法找到数据了。
    CPU最大能查找多大范围的地址叫做寻址能力,CPU的寻址能力以字节为单位,按字节寻址。

  • 通常人们认为,内存容量越大,处理数据的能力也就越强,但内存容量不可能无限的大,它要受到系统结构、硬件设计、制造成本等多方面因素的制约,一个最直接的因素取决于系统的地址总线的地址寄存器的宽度(位数)。

  • 计算机的寻址范围由总线宽度(处理器的地址总线的位数)决定的,也可以理解为cpu寄存器位数,这二者一般是匹配的。

  • Intel公司早期的CPU产品的地址总线和地址寄存器的宽度为20位,即CPU的寻址能力为2
    (20)=1024x1024字节=1M;286的地址总线和地址寄存器的宽度为24位,CPU的寻址能力为2
    (24)=16M;386及386以上的地址总线和地址寄存器的宽度为32位,CPU的寻址能力为2 (32)=4096M=4G。
    也就是说,如果机器的CPU过早,即使有很大的内存也不能得到利用,而对于现在的CPU,其寻址能力已远远超过目前的内存容量。

  • 参考原文地址:CPU寻址范围(寻址空间)一系列问题

类比到生活中,一栋宿舍楼相当于一块内存区域,被划分成了一个个宿舍,为了方便管理和找到某一个宿舍,我们给每一间宿舍都编了门牌号,比如307,表示3楼7号,这样很容易就找到它们的位置在哪里了。

2、获取变量的地址

#include<stdio.h>
int main()
{
	//num在内存要被分配 4字节 的内存空间
	int num = 10;
	//%p - 以地址的形式打印
	printf("%p\n", &num);
	return 0;
}

运行结果:004FFD64(取地址 &num 取的是变量num所在内存空间的第一个字节的地址)

image-20210423224308218

在VS2019中,F10进入调试模式,选择调试菜单 - 窗口 - 内存,就可以找到内存窗口

image-20210423181012823

输入 &变量名,取出变量的地址,这个地址是变量所在内存空间的第一个字节的地址

image-20210423182205615

3、地址的存储 - 指针变量

指针变量存放相同数据类型的变量的首地址

//定义一个整型指针变量,存储整型变量num的地址
int num = 10;
int* p = &num;
//字符型指针变量指向字符型变量
char ch = 'b';
char* str = &ch;

指针变量的使用举例:通过指针变量存放的变量的地址,找到所指向的变量

#include<stdio.h>
int main()
{
	int num = 10;
    //指针变量 p
	int* p = &num;
    //* 是 解引用操作符 / 间接访问操作符,访问指针 p 指向的内存空间
	*p = 20;
	
    printf("num = %d\n", num);
	return 0;
}

运行结果:num = 20

4、指针变量的大小

任何类型的指针变量在 32位平台 是4个字节大小,64位平台 是8个字节大小

指针变量占几个字节跟语言无关,取决于变量的地址的存储需要多大的空间,进一步讲,是与系统的寻址能力有关*(提示:结合上面内容 - 1、什么是内存 - 内存是如何编号的? - 进行理解)*

32位系统 - 32根地址总线 - 一个地址是 32 个比特位 - 4字节

64位系统 - 64根地址总线 - 一个地址是 64 个比特位 - 8字节

所以,不管你是整型变量还是字符型变量还是浮点型变量,在 32/64 位平台中,你的第一个字节(首地址)的编址是由 32/64 个比特位组成的二进制序列,占 4/8 个字节

image-20210424163446947
#include<stdio.h>
int main()
{
	printf("%d字节\n", sizeof(char *));
	printf("%d字节\n", sizeof(int *));
	printf("%d字节\n", sizeof(double *));
	return 0;
}

运行结果:在(32位平台)均输出 4字节、在(64位平台)均输出 8字节

  • VS2019中,这里可以切换程序运行的平台 x86(32位) / x64(64位)
image-20210424165217713

(13)结构体

结构体是C语言中特别重要的内容,结构体使得C语言可以描述复杂类型

比如描述一个人,用之前学过的任何一种类型变量都不足以描述清楚,int? float? char?,似乎都没内味,人是一个复杂对象,有姓名,年龄,性别等等;描述一本书,有作者,定价,书类等等

而我们用结构体,就可以创造一些类型,来描述这些复杂类型了

接下来,我们来创建一个学生的结构体

struct Student
{
	char name[20];  //姓名
	int age;  //年龄
	double grade;  //成绩
};

结构体变量的定义与初始化

  • 定义时初始化
  • 定义之后初始化

一般在定义时初始化,比较方便;定义之后初始化,就只能对每一个成员一一赋值初始化了

//定义一个结构体变量并初始化
struct Student s1 = { "玛卡巴卡",20,85 };
//定义之后初始化
strcpy(s1.name, "李四");  //因为name是数组名,它是一个地址,不能用 s1.name = "李四";
						 //strcpy - 字符串拷贝函数 - 库函数 - 引用头文件string.h
s1.age = 25;
s1.grade = 90;

访问结构体变量中的成员

//方法(1):结构体变量.结构体成员
printf("name: %s age: %d grade: %lf\n", s1.name, s1.age, s1.grade);
	
//方法(2):结构体指针->结构体成员
struct Student* ps = &s1;
printf("name: %s age: %d grade: %lf\n", ps->name, ps->age, ps->grade);

//方法(3):太麻烦,建议用 ->操作符 访问更直观
printf("name: %s age: %d grade: %lf\n", (*ps).name, (*ps).age, (*ps).grade);
  • 14
    点赞
  • 0
    评论
  • 7
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
©️2021 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值