前言
此文章为c语言高级教学续作,有需要可以直接去结语找上部分。由于是初学,很多内容都是点到为止。难免有错误,请各位读者指正。
1.数组
1.1数组的定义
要存储1-10的数字,怎么存储?
C语言中给了数组的定义:一组相同类型元素的集合
int arr[10] = {1,2,3,4,5,6,7,8,9,10};//定义一个整形数组,最多放10个元素
这里我们定义了一个数组,数组名是arr, [10]里面的是元素的个数,int是数组元素的类型。
1.2数组的下标
C语言规定:数组的每个元素都有一个下标,下标是从0开始的。
数组可以通过下标来访问的。
int arr[10] = {0};
//如果数组10个元素,下标的范围是0-9
int arr | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|---|---|---|
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
小插曲之for循环语句讲解
在此之前,我们先讲一下for循环,有助于理解下面的代码。
1.for循环的语法
for(表达式1; 表达式2; 表达式3)
循环语句;
- 表达式1
表达式1为初始化部分,用于初始化循环变量的。 - 表达式2
表达式2为条件判断部分,用于判断循环时候终止。 - 表达式3
表达式3为调整部分,用于循环条件的调整。
这样不太好理解,我们看一个实际的问题:
请用for循环 在屏幕上打印1-10的数字。
#include <stdio.h>
int main()
{
int i = 0;
//for(i=1/*初始化*/; i<=10/*判断部分*/; i++/*调整部分*/)
for(i=1; i<=10; i++)
{
printf("%d ", i);
}
return 0;
}
2.for循环的执行流程
1.3数组的使用
#include <stdio.h>
int main()
{
int i = 0;
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
for(i=0; i<10; i++)
{
printf("%d ", arr[i]);//通过遍历数组下标,打印数组中的元素
}
printf("\n");
return 0;
}
1.3变长数组
int main()
{
int arr1[10] = {0};
int arr2[2 + 8] = {0};
//上述了两个数组的定义均正确
//在C89标准中数组元素个数的定义必须为常量表达式
int n = 10;
int arr3[n];
//上述了这个数组的定义正确
//在C99标准中支持了变长数组,数组的元素可以是变量,在C99标准中arr3,arr2,arr1的定义都是没有问题的
//int arr3[n] = {0};对变长数组进行初始化,是不正确的
return 0;
}
注意:这个变长数组是不能初始化的,而且由于VS对C99语法的支持较差,所以这个代码在VS上编译会报错。
2.操作符
2.1算术操作符
- 加 - 减 * 乘 / 除 % 取余
#include<stdio.h>
int main()
{
int a = 10;
int b = 20;
printf("%d %d %d %d %d\n",a + b,a - b,a * b,a / b,a % b,b / a);//30,-10,200,0,10,2
a += b;//a = a + b
a -= b;
a *= b;
a /= b;
a %= b;
printf("%d %d %d %d %d\n",a,a,a,a,a);//30,-10,200,0;10
int c = 9 / 2;//4
float c = 9 / 2;//4.000000
float d = 9.0 / 2;//4.500000
float d = 9 / 2.0;//4.500000
return 0;
}
1.+,-,*,/应该不用说了,就和我们现实中一样使用。但是这里要注意一点,因为a,b都是整形数,所以a / b的结果也是整型数,但是a / b是小数,只能舍弃小数,结果就是0,但是b / a的结果是整数,就保留。如果两个数相除,结果为小数,保留整数,舍弃小数。
2.%是取余的意思,意思是a / b剩余的余数,%是双目操作符,要求运算的数字必须是整数,上面的a / b = 0,余数是0。
3.当我们进行小数运算时,需要除号两边的数字其一为浮点数即可。其中浮点数默认为double类型!
4.还有一点,a += b等价于a = a + b, + - * /都可以这样,这是为了简化书写。
小插曲之二进制
需要明白二进制才能看懂后面的操作符,给大家推荐一个还不错的文章,讲二进制的!
2.2移位操作符
<< >>
int main()
{
int a = 39;
int b = a << 1;
int c = a >> 1;
printf("%d%d\n",b,c);//78 19
return 0;
}
2.3位操作符
& ^ |
int main()
{
int a = 3;
int b = 5;
int c = a & b;
int d = a | b;
int e = a ^ b;
printf("%d\n%d\n%d\n",c,d,e);
return 0;
}
2.4赋值操作符
= += -= *= /= &= ^= |= >>= <<=
一样的套路,这里咱就不废话了。
2.5单目操作符
逻辑反操作 !
负值 -
正值 +
二进制按位取反 ~
取地址 &
解引用操作符 *
sizeof 操作数的类型长度(以字节为单位)
– 前置、后置–
++ 前置、后置++间接访问操作符(解引用操作符)*
(类型) 强制类型转换
这个不太好说,我们结合代码看看。
1.逻辑反操作!
//注意:在c语言中,0表示假,非0表示真(一般取1为真)
int flag = 5;
if(!flag)
{
prinf("haha\n");
}
2 负值 - 正值 +
int a = -12;
int b = -a;//12
printf("%d\n",b);
int c = +a;//-12 //负数前面加一个+,仍然是负数
printf("%d\n",c);
3 ~二进制按位取反
int main()
{
int a = 1;
int b = ~a;
//先将a转为二进制数,再将符号位和数字为全部取反。
//通俗的说,就是把1全部变成0,把0全部变成1。
return 0;
}
不懂没关系,有个印象就可以了,以后再细说。
4 取地址 & 解引用操作符 *(先不讲,指针后面会讲)
5 前置,后置++ 前置,后置–
int a = 12;
int b = a++;//后置++,先赋值,在自增
//先赋值b = a,再自增a = a + 1
b = 12,a = 13
-----------------------------
int a = 10
int b = ++a;//前置++,先自增,在赋值
//先自增a = a + 1,再赋值b = a
b = 11,a = 11
-----------------------------
/,后置-- 用法一样
int c = 15;
int d = c--;//d = 15,c = 14
int e = 11;
int f = --e;//f = 10,e = 10 sizeof 操作数的类型长度(以字节为单位)
6 强制类型转化 ()
c int a = (int) 2.31; //()把浮点数2.31强制转化为整形数2,转化的过程中会发生数据截断,0.31被截断丢弃 //浮点数一般不标识数据结构,字面浮点数默认为double类型
7 sizeof 操作数的类型长度(以字节为单位)
int a = 0;
//%zu是标准用于计算sizeof
printf("%d\n",sizeof(a));//4
printf("%zu\n",sizeof(int));//4
//当sizeof后面跟数据类型时,打印的是此类型的字节长度,()不可与省略,
printf("%zu\n",sizeof a);//4
//后面是变量,()可省略不写,同时说明sizeof不是一个函数
---------------------------
int arr[10] = {0};
printf("%d\n",sizeof(arr));//40
//此时计算的是整个数组的大小,单位是字节
printf("%d\n",sizeof(arr[0]));//4
//此时计算的是数组第一个元素的大小
printf("%d\n",sizeof(arr) /sizeof(arr[0]));//10
//此时计算的数组的个数
2.6关系操作符
>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
这个没什么好讲的
大于 >= 大于等于 < 小于 <= 小于等于
/这里重点强调一下 c语言中等号是== ,=是赋值的意思,初学者容易混淆,我曾经就犯过这种错误
2.7逻辑操作符
&& 逻辑与(也叫短路与)
|| 逻辑或(也叫短路或)
int a = 0;
int b = 0;
int c = 1;
int d = 2;
if(a && b)//a,b均为假,条件为假,不打印
{
printf("假");
}
if(a || b)//a,b均为假,条件为假,不打印
{
printf("假");
}
if(a || c)//a,c一真一假,条件为真,打印
{
printf("真");
}
if(a && d)//a,d均为真,条件为真,打印
{
printf("真");
}
if(a && c)//a,c一真一假,条件为假,不打印
if(c || d)//a,d均为真,条件为真,打印
&& 必须是两边的条件均为真,才执行
|| 当两边的条件有一个为真,就执行
为什么又叫短路与和短路或呢?
&& 第一个条件为假时,后面的就不用判断了,语句为假
|| 第一个条件为真时,后面的就不用判断了,语句为真
2.8条件操作符
exp1 ? exp2 : exp3
//三目操作符
//当exp1为真时,执行exp2,当exp1为假时,执行exp3
int a = 34;
int b = 23;
int c = a > b? a : b//求a,b的最大值
2.9逗号表达式
exp1, exp2, exp3, …expN
//逗号表达式就是用逗号隔开的一串表达式
//特点是:从左到右依次计算各个表达式,
//整个逗号表达式的结果是最后一个表达式的结果
#include<stdio.h>
int main()
{
int a = 12;
int b = 25;
int c = 23;//a = 8 b = 33 c = 19 a = 22
int ret = (a -= 4,b += a,c -=4,a = a + b - c)
printf("%d\n",ret);//22
return 0;
}
2.10下标引用、函数调用和结构成员
[] () . ->
先讲一下前两个,后两个会在结构体中讲到
下标引用操作符 []
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
arr[1] = 23;
//[]就是下标引用操作符,arr和[]就是操作数
//我们可以通过下标访问数组中的元素
函数调用操作符()
int Add(int x,int y)//()就是函数调用操作符
{
return x + y;
}
int main()
{
int sum = Add(3,7)//Add,3,7都是()的操作符
//这里的()不能省略,我们之前说过,sizeof可以省略(),也说明sizeof不是函数
return 0;
}
3.常见关键字
auto break case char const continue default do double else enum
extern float for goto if int long register return short signed
sizeof static struct switch typedef union unsigned void volatile while
关键字 | 关键字作用 |
---|---|
char | 声明字符型变量或函数 |
float | 声明单精度浮点型变量或函数 |
int | 声明整型变量或函数 |
double | 声明双精度浮点型变量或函数 |
void | 声明函数无返回值或无参数,声明无类型指针 |
short | 声明短整型变量或函数 |
long | 声明长整型变量或函数 |
signed | 声明有符号类型变量或函数 |
unsigned | 声明无符号类型变量或函数 |
union | 声明联合数据类型类型 |
sizeof | 计算数据类型所占内存空间大小 |
typedef | 数据类型重命名(定义) |
struct | 声明结构体类型变量或函数 |
enum | 声明枚举类型 |
auto | 声明自动类型变量,缺省时,编译器一般默认为auto型 |
static | 声明静态变量或指定函数是静态函数 |
extern | 声明变量是全局变量,在其它文件中声明(声明外部变量或函数) |
const(ant) | 声明只读变量,其修饰的只读变量必须在定义的同时初始化 |
register | 声明寄存器变量 |
volatile | 说明变量在程序执行中可被隐含的改变 |
break | 跳出当前循环,表示终止本层循环 |
continue | 结束当前循环,开始下一次循环 |
goto | 无条件跳转语句 |
return | 子程序返回语句,终止一个函数并返回值 |
if | 条件语句 |
else | 条件语句否定分支 |
switch | 用于开关变量 |
case | 用于语句分支(后跟整形或字符型常量) |
default | 开关语句中的其他分支 |
for | 一种循环语句 |
do | while循环语句的循环体 |
while | while循环语句的循环体条件 |
变量的命名规则
-
变量名只能由字母,数字,下划线组成,不能有特别字符,且不能以数字开头。
-
建议使用见名知意的名字,可以使用英文字母驼峰模式或加下划线的方式,尽量不要使用汉语拼音。
-
变量名不能是关键字
C语言提供了丰富的关键字,这些关键字都是语言本身预先设定好的,用户自己是不能创造关键字的。 这里我们先讲几个不常见的,长长见识。
3.1关键字 typedef
typedef 顾名思义是类型定义,这里应该理解为类型重命名。
比如:
//将unsigned int 重命名为uint_32, 所以uint_32也是一个类型名
typedef unsigned int uint_32;
int main()
{
//观察num1和num2,这两个变量的类型是一样的
unsigned int num1 = 0;
uint_32 num2 = 0;
return 0;
}
总结下来就是,如果一个类型很复杂,我们可以个他取个简单的别名,偷偷懒了!!!
3.2关键字register
int main()
{
register int num = 100;//声明寄存器变量
//建议把频繁使用的数据放到寄存器中
return 0;
}
CPU – 中央处理器,早期的计算机处理数据从内存中去拿,随着技术的发展,CPU的速度越来越快,但是内存的读写速度跟不上,所以CPU经常处于闲置状态,不能充分运用,于是有人想出在内存之上提供一个高速缓存区,在高速缓存区之上提供一个寄存器空间,把那些频繁处理的数据,放在高速缓存区或寄存器中,提高数据处理速度。
3.3关键字static
在C语言中:
static是用来修饰变量和函数的
\1. 修饰局部变量-称为静态局部变量
\2. 修饰全局变量-称为静态全局变量
\3. 修饰函数-称为静态函数
3.3.1修饰局部变量
//代码1
#include <stdio.h>
void test()
{
int i = 0;
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for(i=0; i<10; i++)
{
test();
}
return 0;
}//结果是2 2 2 2 2 2 2 2 2 2
//代码2
#include <stdio.h>
void test()
{
//static修饰局部变量
static int i = 0;
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for(i=0; i<10; i++)
{
test();
}
return 0;
//结果是2 3 4 5 6 7 8 9 10 11
对比代码1和代码2的效果理解static修饰局部变量的意义。
结论:
static修饰局部变量改变了变量的生命周期
让静态局部变量出了作用域依然存在,到程序结束,生命周期才结束。
3.3.2修饰全局变量
//代码1
//add.c
int year = 2022;
//test.c
int main()
{
printf("%d\n", year);
return 0;
}
//代码2
//add.c
static int year = 2022;
//test.c
int main()
{
printf("%d\n", year);
return 0;
}
代码1正常,代码2在编译的时候会出现连接性错误。
结论:
一个全局变量被static修饰,使得这个全局变量只能在本源文件(test.c)内使用,不能在其他源文件内使用。
全局变量,在其他源文件内部可以使用,是因为全局变量具有外部连接属性,但是被static修饰之后,就变成了内部链接属性,其他源文件就不能链接到这个静态的全局变量。
3.3.3修饰函数
//代码1
//add.c
int Add(int x, int y)
{
return x+y;
}
//test.c
int main()
{
printf("%d\n", Add(2, 3));
return 0;
}
//代码2
//add.c
static int Add(int x, int y)
{
return x+y;
}
//test.c
int main()
{
printf("%d\n", Add(2, 3));
return 0;
}
代码1正常,代码2在编译的时候会出现连接性错误.
结论:
一个函数被static修饰,使得这个函数只能在自己所在的源文件内部使用,不能再其他源文件内部使用。
本质:static是将函数的外部链接属性变成内部链接属性!(和static修饰全局变量一样!)
4.define定义常量和宏
define是一个预处理指令,他并不是c语言的关键字
4.1define定义标识符常量
#define MAX 100
int main()
{
printf("%d",MAX);//100
}
4.2define定义宏
#define Add(X,Y) X+Y
int main()
{
printf("%d\n",Add(3,6));//9
printf("%d\n",4 * Add(3,6));//18
//有的小伙伴有疑问了,不是4 * (3 + 6)吗?往下看
return 0;
}
这是因为编译器执行的是:4 * 3 + 5。define定义宏中,我们不应该把参数X和参数Y当成一个普通的变量,他横可能是表达式。
#define Add(X,Y) ((X) + (Y))
所以我们平时初始化时,应该把参数括起来,再把宏体括起来。
5.指针
终于来啦,那个被誉为c语言的灵魂的指针他来了!我们总是听老师,学长学姐们说指针很难,大家先不要害怕,其实他也没那么难。
5.1指针的概念及内存
首先,我们要知道程序都是在内存中运行的,内存是计算机中特别重要的存储器,早期的科学家为了有效地使用内存,就把内存划分为了一个个小小的内存单元,每个内存单元的大小都是一个字节,每个内存单元都有一个独特的编号,这个编号叫做这个内存单元的地址。编号可以暂时理解为指针。
举个例子来说,就比如我们把内存比作一个大酒店,内存中每个小小的内存单元就好似酒店中一个个房间,每个内存单元都有一个独特的编号就好似每个酒店房间都有一个门牌号。
我们分析一下,我们可以在内存中可以看到地址,内存中的数据和内存数据的文本解析(内容不定,没什么用)。我们知道整形在内存中分配了4个字节的空间,我们看a的地址和她下面的地址相差4(16进制),因为int类型的数据占了4个字节。a =10,用二进制来表示就是
0000 0000 0000 0000 0000 0000 0000 1010,转化为二进制就是00 00 00 0a,但是我们发现a的数据是倒置的,其实这是小端存储方式,这里就先不介绍了。
5.2地址的生成
但是内存单元是如何编号的?以早期的32位电脑为例,他有32根地址线,每根地址线可以产生1/0两种电信号,每个地址线组合起来就会有许多不同的排列方式。
一共有2的32次方种排序方式,(2^32 = 4294967296),内存中就有这么多字节的空间。通过下图的转换,我们发现,在早期的32位电脑内存中一共有4GB的内存空间。也就是说计算机总共可以管理分配4GB的内存。现在我们身边的电脑大多是64位的,可以容纳16GB,32GB的内存。
5.3变量与地址
5.3.1变量的指针和指针变量
变量是创建内存中的(在内存中分配空间的),每个内存单元都有地址,所以变量也是有地址的。
取出变量地址如下:
#include <stdio.h>
int main()
{
int num = 10;
#//取出num的地址
//注:这里num的4个字节,每个字节都有地址,取出的是第一个字节的地址(较小的地址)
printf("%p\n", &num);//打印地址,%p是以地址的形式打印
return 0;
}
那地址如何存储,需要定义指针变量。指针的使用实例:
int num = 10;
int *p;//p为一个整形指针变量
p = #
总结:变量的指针就是变量的存储地址,指针变量就是存储指针的变量。
5.3.2指针变量的使用
-
取地址运算符&:单目运算符&是用来取操作对象的地址。例:&i 为取变量 i 的地址。对于常量表达式、寄存器变量不能取地址(因为它们存储在存储器中,没有地址)。
-
解引用操作符(间接寻址符) *:与&为逆运算,作用是通过操作对象的地址,获取存储的内容。例如:x = &i,x 为 i 的地址,
*x 则为通过 i 的地址,获取 i 的内容。
#include <stdio.h>
int main()
{
int num = 10;
int *p = #//这个*说明p是一个指针变量
*p = 20;//与上面的*不同,这个*是解引用操作符
return 0;
}
我们来分析一下上述过程,加强大家对解引用操作符的理解,int是整形数据结构,占4个字节,所以num占4个字节的内存,存入了整型数10,&num取出了num的第一个地址(通过第一个地址向上继续访问)存入了指针变量p中,p指向num,p指向的类型为int类型,所以*p前面类型为int,这个int指的是p指向的类型。对指针变量解引用,发现了num的地址,通过p中的地址指向了num,并把num中的值改为了20。
解引用操作符 *:就比如有一个包裹,我们解引用一下,打开了这个包裹把里面的东西拿过来引用一下。解引用p就等于打开p的空间,取出地址,通过地址找到num的内存,并获取num中的值。
5.3.3“&” 和“*”结合方向
“&” 和“*”结合方向都是左结合(编译器喜欢),其实左右都可以。假设有变量x = 10,则 *&x的含义是,先获取变量x的地址,再获取地址中的内容。因为“&”和“ *”互为逆运算,所以x = *&x。
int main()
{
int* p = &a;
int *p = &a;
//以上均可
//但是在连续定义是会有区别
int* p1,p2,p3 =&a;
//这个定义有问题,只有p1是指针,p2,p3是整形变量
int *P1,*p2,*p3;
//这个也有问题,往下看
return 0;
}
5.3.4其他指针
以整形指针举例,可以推广到其他类型,如:
#include <stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch;//字符指针
*pc = 'q';
printf("%c\n", ch);
return 0;
}
5.3.5指针变量的初始化
指针变量与其它变量一样,在定义时可以赋值,即初始化。也可以赋值“NULL”或“0”,如果赋值“0”,此时的“0”含义并不是数字“0”,而是 NULL 的字符码值。注意一定要初始化,否则就是野指针,我们也不知道他会指向哪里。
int x;
int *px = &x;
//错误的定义方法,这是个野指针
int *P1;
//正确的定义指针变量的方法
int *p2 = NULL,int *p3 = 0;
5.3.6指针变量的大小
#include <stdio.h>
//指针变量的大小取决于地址的大小
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
printf("%d\n", sizeof(char *));
printf("%d\n", sizeof(short *));
printf("%d\n", sizeof(int *));
printf("%d\n", sizeof(double *));
return 0;
结论:指针大小在32位平台是4个字节,64位平台是8个字节。
6.结构体.
6.1.结构体是什么
结构体可以说是人为构造的数据类型,它使c语言有能力描述复杂的数据类型。比如在程序中描述一本书的价格,一个人的身高,人的姓名等等,可以用char型,int型数据类型。但是要是需要描述一个学生,一个学生包含了:姓名+年龄+性别+电话
我们用基本的数据类型无法描述这种复杂的对象,这里我们就可以使用结构体来描述它,结构体可以让c语言创建出新的类型出来。
//创建一个学生类型
struct Stu
{
//成员变量
char name[20];//姓名
int age;//年龄
char sex[10];//性别
char tele[12];//电话
}; //;是结束标志
6.2结构体的使用
void print(struct Stu* ps)//用结构体变量接收
{
//ps指向s的地址,*解引用一下,找到s中的值
printf("%s %d %s %s\n",(*ps).name,(*ps).age,(*ps).sex,(*ps).tele);
//-> 结构体指针变量->成员变量名
//通过ps找到结构体对象,然后通过->找到他里面的成员变量
printf("%s %d %s %s\n",ps->name,ps->age,ps->sex,ps->tele);
}
int main()
{
//注意:一定要一一对应
struct Stu s = {"张三",20,"男","13573282919"};//结构体的创建和初始化
//结构体对象.成员变量名
printf("%s %d %s %s\n",s.name,s.age,s.sex,s.tele);
//这里用到了一个.操作符,找到结构体成员并访问它用.操作符
struct Stu *ps = &s;
//结构体类型的指针,把s的地址存到结构体指针中去
print(&s);//将结构体指针传入函数
return 0;
}
结语
各位小伙伴们,祝贺你们终于学完了初识c语言,相信大家应该对c语言有个大致的了解,但是这些知识还还远远不够,以后将会更详细的讲解新的知识,你们准备好了吗,让我们一起扬帆起航!!!
如果各位小伙伴们觉得我写得不错,或者看完后有所收获,麻烦大家给个三连(关注,点赞,评论),你们的支持就是我最大的动力!!!
为帮助大家理清脉络,把c语言上部分放到下面!!!(点link)