目录
前言
hello,大家好,学完C语言已经好久了,一直想写一些总结,也许这份总结不够全面,不够细致,甚至难以保证每一个点都精确,但是这是一个菜鸟对自己一段学习经历的致敬,希望大家多多支持。
下面,就有请大家和我一起,重温C语言。
1. 从Hello,world开始
每一个接触计算机编程的人,几乎都是从Hello,world开始的,这是我们在电脑上写下的第一个程序。我们不会忘记,那最初一行输出是多么使我们惊喜。
至于为什么是Hello,world,这是有一段来历的,下面是百度百科到的故事。
其实也不难理解,当我们创造了新的事物,并期待着这个事物可以带给我们自己收获感,甚至带给世界改变的时候,我们的第一想法,难道不是大声的告诉这个世界吗?所以,Hello,world,让我们一起相信,技术改变世界。
2. 数据的类型与运算
2.1 整形在内存中的存储
我们从小学就开始学数,从0、1、2、3……这种简单的数,到成千上万的那种大数,再到小数,负数,分数等不同类型的数字。而在计算机中,数也分为不同的类型,而这不同的类型的分类标准在于,在内存中的存储的不同。在博主之前的文章整数在内存中的存储中,博主已经大概介绍过不同的数据类型及其存储方式,需要的小伙伴可以点击查看一下。下面,博主将再一次简要介绍一下相关内容。
2.1.1 从二进制说起
在计算机中,数据都是以二进制进行存储的。所以要了解数据在内存中的存储方式,我们首先要了解二进制。
相对于我们熟悉的十进制,二进制是以0和1两个数来表示数的一种计数方式。0,就是没有,我们可以理解为“无”,而1我们则可以理解为“有”,老子说“有无相生”,也就是有和无因互相对立而依存,所以,二进制这种表示数的方式,也充满了一定的哲学意味。有时候我们不得不佩服有设计者的精妙,我们只要用0和1两个数字,就能表示出整个庞大体系。而我们周围的世界,无非是看到则有,不见则无。这其中暗含的联系,真是十分有趣。
那么,如何用二进制来表示一个数?
我们以32这个数字为例,在十进制中是这样的。
那么在二进制中,我们则需考虑,如何将32转换为2的几次幂的和呢?
这里,我们将采用短除法。
所以,32的二进制表示为100000.
于是,我们可以发现,所谓的短除法,就是用该数字的十进制表示的数字不断除以2,记录下每一次的余数,然后将余数从下往上输出,得到的就是该数字的二进制表示方法。
同理,求8进制和16进制都是类似方法。
2.1.2 数据类型
2.1.2.1 数据类型家族
1.char 字符数据类型
2.short 短整型
3.int 整形
4.long 长整形
5.long long 更长的整形
6.float 单精度浮点型
7.double 双精度浮点型
以上为基本的内置类型。如果我们再细致一些,可以有如下分类方法:
1.整形家族
char:
unsigned char
signed char
short:
unsigned short [int]
signed short [int]
int:
unsigned int
signed int
long:
unsigned long [int]
signed long [int]
2.浮点型家族
float
double
3.构造类型
a.数组类型
b.结构体数组 struct
c.枚举类型 enum
d.联合类型 union
4.指针类型
int *p
char *p
float *p
void *p
5.空类型
void(通常应用于函数返回类型、函数的参数、指针类型)
注意,上面的signed是指有符号的,而unsigned是指无符号的。在64位的计算机中(现在的计算机基本都是64位的),char类型存储占用一个字节,int占4个字节,float占4个字节,double占8个字节。
2.1.2.2 比特、字节……
在上面我们提到int占4个字节等等,那么什么又是字节呢?了解字节前,我们首先要了解比特。在计算机中,比特(bit)为最小的存储单位,一个数字0或1就代表一个比特位。而每8个比特位就是一个字节(Byte)。字节是计算机中数据处理的基本单位。
于是,我们可以得到如下的单位换算:
1Byte=8Bit
1KB=1024Byte
1MB=1024KB
1GB=1024MB
2.1.3 原码、反码、补码
要了解整形是如何在内存中存储的,首先要明确有符号整数在内存中的三种存储方式:原码、反码、补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位三种表示方法各不相同。
原码
直接将二进制按照正负数的形式翻译成二进制就可以。
反码
将原码的符号位不变,其他位依次按位取反就可以得到了。
补码
反码+1就得到补码。
注意:
正数的原、反、补码都相同。
对于整形来说:数据存放内存中其实存放的是补码。
至于为什么内存中存放的是补码,在上面链接的文章中博主已经介绍过了,就不在进行详细介绍。
这是一个数字原码、反码和补码求得的例子:
从中,有一些问题值得我们注意,第一,既然表示整形的字节数有限,所以,整形可以表示的数字的范围也是有限的。同时,在进行一些问题的求解时,要注重原码反码和补码的转换。记住,内存中存的是补码,这是进行许多相关计算的基础。
2.1.4 整形提升
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0
2.2 实型、字符型
2.2.1 实型
单精度实型为float占4字节,双精度为double占8字节。在内存中,实型数据被分为符号位(1位),小数部分和指数部分。小数部分和指数部分位数由于编译器的不同而不同。但,小数部分位数越多,数据精度越高。指数部分位数越多,数据范围越大。
2.2.2 字符型
字符型为char。占用1个字节。存储的是字符的ASSIC码值。
这里我们上一张ASCII码的表,ASSIC在我们做一些题的时候要经常用到。
2.3 常量和变量
常量:
常量是数据运行中不能改变的数据,变量表示程序运行中可以发生变化的数据。下面我们将来介绍他们。
2.3.1 字面常量
字面常量是以字面格式直接写在程序中的,字面常量用于不同数据类型表示时,有不同的格式要求。
2.3.1.1 整形常量
在C语言中,整形常量有三种字面格式:
1.十进制数字:与整数写法无异。
例:-11、10、1238……
2.八进制数字:在八进制前加数字0.
例:-011、010、01238……
3.十六进制:在十六进制前加0x。
例:-0x11、0x10、0x1238……
2.3.1.2 实型常量
实型常量只能用十进制来表示。有两种格式如下:
1.一般格式:-12.345、0.0、12.34……有时候小数点前后的0可以省略。
2.指数格式:-1.2345e2等。由尾数,字母e或E、指数部分组成。按照双精度进行存储。
2.3.1.3 字符型常量
字符型常量用单引号(’’)括起来。注意,是因为状态下的单引号。同时,在C语言中,有一些控制固定格式的字符无法打印出来,这是我们需要用到,来使得字符可以打印出来。
那么转义字符都有什么呢?看下表:
于是,这里诞生了一句绕口令:斜杠的作用是使斜杠打印在屏幕上。
至于这句话是什么意思,我们以\n控制换行为例,来看一下:
输出为一块空行。
输出为\n/
通过对比我们发现,\n的基础上在加,\n就失去了他的转义功能,是的像普通字符一样,可以打印在屏幕上。
注意:当我们了解了转义字符的概念之后,我们在今后的一些求字符串的长度的问题里,就要切记,转义字符要当作一个字符来看待。
2.3.1.4 字符串常量
字符串常量是由双引号("")括起来的若干字符。其中包含的字符数可长可短可为0.字符串在内存中由字符表示组成,这也意味着,字符串的内存大小与其包含的字符数有关。同时要注意,在字符串的结尾,有1个字节的空间存储字符‘\0’,用以标记字符串的结束。从此,我们要注意,字符串的实际长度要在字符的个数上再加1.
2.3.2 const修饰的常变量
当我们把一个变量用const修饰时,该变量的值便不可再改变,此时变量具有常属性。
2.3.3 define定义宏常量
在同一个程序中,某些字面常量可能会使用很多次,为了更加方便,我们可以用预处理命令#define来定义符号常量。
格式如下:
#define 符号常量 常量值
宏定义之后,符号常量将代表该值在程序中出现。相当于古代皇帝赐给臣子的令牌,这个令牌的持有者可以代表皇帝发号施令。
注意,宏替换语句后面没有分号。C语言中一般将宏的符号用大写字母来表示以便于区分。
并且,使用宏的过程中我们会遇到一些坑,比如下面这个问题
#include<stdio.h>
#define R 2+3
#define A R*R
int main()
{
printf("%d\n", A);
return 0;
}
请试想一下这段代码的输出结果是什么?我们掐指一算,自信地说25嘛。可惜我们错了,答案是11.
为何结果如此有悖于我们的认知?我们来分析一下:记住宏是替换,所谓的替换就是说,我们要原封不动的搬下去。我们在定义R 2+3时,我们是要用2+3替换R而不是用5来替换R。所以,A最终的表达式是2+3*2+3.完全将R代进去,不要一厢情愿地加入自己的算术规则。
变量:
当我们看到变量这个名字,大概就可以猜出,变量不同于常量在于,变量是可以改变的。变量的本质是一个内存单元,这块内存不变,但内存里放的东西可以改变。比如你有一个漂亮的盒子,你今天可以用它放一根笔,明天可以把笔拿出来在里面放一张卡片,盒子里的东西你可以根据喜好更换,但,盒子还是那个盒子。
2.3.4 变量的命名
变量名有一个标识符来表示,标识符只能由字母数字和下划线组成,并且第一个字符必须是字母或下划线,而不可以是数字。标识符对于字母大小写敏感。Book和book是两个不一样的变量名。并且要注意,不要使用系统中例如int、for、define等关键字来做变量名,否则编译器会傻傻分不清。
2.3.5 变量的定义和初始化
一条变量定义语句,可以定义多个相同类型的变量,系统会为每一个变量分配一个连续的空间,空间的大小取决于变量的类型。变量的在使用前必须定义,否则是严重的语法错误。
并且,良好的保持风格要求对变量进行初始化,这样做的优点在于,可以见啥几条赋值语句,同时也避免出现变量去随机值的现象。
常量变量之思
此段为瞎扯,不涉及知识,读者可跳过。常量是不变的,而变量是变的。想起电影《无问西东》的一句台词“人就不能变吗?”
又有那一句歌词“世上唯一不变,是人都善变”。所以,即使我们总在期待常量,但生活中更多的还是变量。而生活充满变量这句话本身,是我们为数不多可以相信的常量。只是觉得这样看待起问题来,还蛮有趣的。
2.4 数据的基本运算
2.4.1 运算符简介
2.4.2 算术运算符
+、-、*、/、%
1.除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
2. 对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
3. % 操作符的两个操作数必须为整数。返回的是整除之后的余数。
2.4.3 移位操作符
左移操作符 移位规则:
左边抛弃、右边补0
右移操作符 移位规则:
首先右移运算分两种:
- 逻辑移位 左边用0填充,右边丢弃
- 算术移位 左边用原该值的符号位填充,右边丢弃
警告 : 对于移位运算符,不要移动负数位,这个是标准未定义的。 例如:
位操作符
int num = 10;
num>>-1;//error
2.4.4 位操作符
按位与为两位都是1为1,其余为0.
按位或为两位有一个是1,就为1.
按位异或则是两个数不同则为1,同则为0.即0异或任何数为任何数,1异或任何数为任何数取反,任何数异或自己相当于将自己置0。
根据位操作符的特点,我们可以在不设置第三个变量的情况下,实现两个数的交换。
#include <stdio.h>
int main()
{
int a = 10;
int b = 20;
a = a^b;
b = a^b;
a = a^b;
printf("a = %d b = %d\n", a, b);
return 0;
}
并且,两个数字的按位与和按位或的和等于两个数的和。
2.4.5 赋值操作符
=
记住,在C语言中,一个等于号是赋值,两个等于号才相当于我们数学中的=。
并且赋值符可以和其他符号共同构成复合赋值符。
2.4.6 单目操作符
注意前置和后置++的区别。前置是先+1,后置是后+1.下面的程序就可以体现出二者的差别。
那么- -同理。
2.4.7 关系操作符
2.4.8 逻辑操作符
注意,逻辑与和逻辑或要区别于按位与和按位或。逻辑与和逻辑或是用在两个表达式中。
2.4.9 条件操作符
表示如果满足exp1则取exp2,不满足则取exp3.
2.4.10 常见算术运算符优先级口诀(博主原创哦)
加加减减非常先
乘除模加减后面
大于小于随其后
等于不等跟上前
和或总在最后边
2.5 数据类型转换
自动类型转换会从空间小的转向空间大的,强制类型转换可以根据需要转换。比如int和float一起时,int——>float.
强制类型转换的格式为(类型名)表达式。
3. 数据的输入输出
3.1 C语言的输入输出
C语言的输入输出通过标准输入输出库stdio.h中声明的函数来实现。
3.2 字符的非格式化输入输出函数
getchar()和putchar()
#include<stdio.h>
int main()
{
char c;
c = getchar();//从键盘上输入一个字符
putchar(c);//输入刚刚输入的字符
putchar('\n');//换行
putchar(c+1);//输出输入的字符的下一个字符
putchar('\n');
return 0;
}
3.3 格式化输出函数printf
printf()是非常重要的函数,而它涉及到的格式字符众多,我们难以全部记住,只需要记住一些常用的就好,其他的就交给百度。
3.3 格式输出函数scanf
这些是常用的。并且注意,在使用scanf时,不要忘记&。
4.程序的基本结构与基本语句
4.1 C语言中的基本语句
4.2 C程序基本结构组成
4.2.1 顺序结构
这种结构的语句只有一个入口和一个出口,按照从上到下的顺序执行。
4.2.1.1 赋值语句
变量名=表达式
在进行赋值运算时,若两边类型不一致,会把右边的数据类型转换为左边的。并且,赋值语句不能出现在表达式中(int a=b=c=5不合法)。
4.2.1.2 逗号表达式
逗号的优先级最低,功能是从左到右依次求解每一个表达式的值。
4.2.2 分支结构
分支语句也称为选择语句,能够控制程序根据给定的条件,选择执行两个或两个以上分支程序段中的某一个。
4.2.2.1 单分支if语句
if(表达式)语句
如果表达式为真,则执行语句,否则不执行。
注意:如果要执行的语句为多行,要用花括号。
4.2.2.2 双分支if语句
if(表达式)
语句1
else
语句2
如果表达式值为真,则执行语句1,否则执行语句2.
注意:在使用if语句时,if后的表达式,通常是逻辑表达式或者关系表达式,但也可以是其他表达式如赋值表达式甚至是一个变量。如果是赋值表达式恒为真。
4.2.2.3 多分支if语句和if语句的嵌套
注意:if语句可以嵌套,但是C语言规定,else总是与它前面没有配对的if配对。
4.2.2.4 条件运算符与条件表达式
4.2.2.5 switch语句(开关语句)
4.2.3 循环结构
4.2.3.1 while循环
循环打印的一个例子:
#include <stdio.h>
int main()
{
int i = 1;
while(i<=10)
{
printf("%d ", i);
i = i+1;
}
return 0;
}
注意:
break在循环中的作用:
其实在循环中只要遇到break,就停止后期的所有的循环,直接终止循环。 所以:break是用于永久终止循环的。
continue在循环中的作用就是:
continue是用于终止本次循环的,也就是本次循环中continue后边的代码不会再执行,而是直接跳转语句的判断部分。进行下一次循环的入口判断
4.2.3.2 for循环
for和while的比较:
int i = 0;
//实现相同的功能,使用while
i=1;//初始化部分
while(i<=10)//判断部分
{
printf("hehe\n");
i = i+1;//调整部分
}
//实现相同的功能,使用while
for(i=1; i<=10; i++)
{
printf("hehe\n");
}
可以发现在while循环中依然存在循环的三个必须条件,但是由于风格的问题使得三个部分很可能偏离较远,这样查找修改就不够集中和方便。所以,for循环的风格更胜一筹。 for循环使用的频率也最高。
注意:
- 不可在for 循环体内修改循环变量,防止 for 循环失去控制。
- 建议for语句的循环控制变量的取值采用“前闭后开区间”写法。
4.2.3.3 do while循环
注意:该循环至少执行一次,使用的场景有限,所以不是经常使用。
4.2.3.4 循环的嵌套
在二维数组的输入输出等问题中,会经常用到循环的嵌套。
4.2.3.5 三种循环的比较
5. 函数和模块化程序设计
5.1 模块化程序设计
5.2 用户自定义函数
5.2.1 函数的定义与声明
注意,函数的声明和定义可以分离。
ret_type fun_name(para1, * )
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
例如,我们可以写一个可交换两个变量的函数
void Swap1(int x, int y)
{
int tmp = 0;
tmp = x;
x = y;
y = tmp;
}
5.2.2 函数的参数
一定要注意区分哦。
实际参数(实参):
真实传给函数的参数,叫实参。实参可以是:常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
形式参数(形参):
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。形参实例化之后其实相当于实参的一份临时拷贝。
5.2.3 函数的调用
传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。这种传参方式可以让函数和函数外边的变量建立起正真的联系,也就是函数内部可以直接操作函数外部的变量
函数的调用可以嵌套。
5.3 系统库函数
C语言提供了大量库函数来供读者使用。使用库函数可以简化编程工作量。但是注意,库函数并不是C语言本身的一部分,而是根据需要编制的一组程序。
可以说,库函数相当于一个工具箱。我们在这里只介绍一些常用的库函数。Cplusplus官网
我们也可以通过官网来查询库函数,点击上面的网址即可。
5.3.1 头文件与文件包含
常见的头文件如下:
5.3.2 数学函数
其中,数学函数math.h中包含了一些很实用的函数,可以解决一些运算问题,如下:
5.3.3 随机函数
5.4 变量的作用域和生命周期
5.4.1 变量的作用域
5.4.1.1 局部变量
在函数内部定义的变量,包括形参。局部变量只能在所属函数内部起作用。不同函数的局部变量可以重名。
还有一类更小的局部变量可以定义在复合语句中。
5.4.1.2 全局变量
定义在函数外部的变量,从定义之处到程序末尾的所有函数皆为其作用域。
5.4.1.3 重名问题
全局变量不能重名,同一个函数中局部变量也不可重名。若在某个程序中,局部变量与全局变量重名,局部变量在其作用域中屏蔽全局变量。正所谓强龙压不过地头蛇。
5.4.2 变量的生存期
注意:变量的全局和局部性,以及生存周期常常设坑。
5.5 函数的递归调用
阶乘问题,汉诺塔问题,青蛙跳台阶问题,斐波那契数列问题等问题中,我们都可以利用函数的递归来解决。
6. 数组
我们常常说物以类聚人以群分,也就是说,相同属性的事物是常常在一起的。那么在程序设计中,当我们需要处理大量相同属性的数据时,如果我们一个一个地去定义变量岂不是太麻烦了?于是我们引入数组,用来处理大量的相互关联的数据。
6.1 一维数组
6.1.1 一维数组的创建和初始化
6.1.1.1. 一维数组的创建
type_t arr_name [const_n];
//type_t 是指数组的元素类型
//const_n 是一个常量表达式,用来指定数组的大小
6.1.1.2 一维数组创建的实例
//代码1
int arr1[10];
//代码2
int count = 10;
int arr2[count];//?
//代码3
char arr3[10];
float arr4[1];
double arr5[20];
注意,在上述实例中,打问号的数组不能创建,因为【】中只能为常量而不能为变量。
6.1.1.3 一维数组的初始化
int arr1[10] = {1,2,3};
int arr2[] = {1,2,3,4};
int arr3[5] = {1,2,3,4,5};
char arr4[3] = {'a',98, 'c'};
char arr5[] = {'a','b','c'};
char arr6[] = "abcdef";
6.1.2 一维数组的使用
#include <stdio.h>
int main()
{
int arr[10] = {0};//数组的不完全初始化
//计算数组的元素个数
int sz = sizeof(arr)/sizeof(arr[0]);
//对数组内容赋值,数组是使用下标来访问的,下标从0开始。所以:
int i = 0;//做下标
for(i=0; i<10; i++)//这里写10,好不好?
{
arr[i] = i;
}
//输出数组的内容
for(i=0; i<10; ++i)
{
printf("%d ", arr[i]);
}
return 0;
}
int arr[10];
int sz = sizeof(arr)/sizeof(arr[0]);
注意:
- 数组是使用下标来访问的,下标是从0开始。
- 数组的大小可以通过计算得到
int arr[10];
int sz = sizeof(arr)/sizeof(arr[0]);
6.1.3 一维数组在内存中的存储
数组在内存中是连续存放的。
6.2 二维数组
6.2.1 二维数组的创建和初始化
6.2.1.1 二维数组的创建
//数组创建
int arr[3][4];
char arr[3][5];
double arr[2][4];
6.2.1.2 二维数组的初始化
//数组初始化
int arr[3][4] = {1,2,3,4};
int arr[3][4] = {{1,2},{4,5}};
int arr[][4] = {{2,3},{4,5}};
6.2.1.3 二维数组的使用
#include <stdio.h>
int main()
{
int arr[3][4] = {0};
int i = 0;
for(i=0; i<3; i++)
{
int j = 0;
for(j=0; j<4; j++)
{
arr[i][j] = i*4+j;
}
}
for(i=0; i<3; i++)
{
int j = 0;
for(j=0; j<4; j++)
{
printf("%d ", arr[i][j]);
}
}
return 0;
}
6.2.1.4 二维数组在内存中的存储
6.3 数组作为函数参数
数组作为参数的时候,不是把整个数组传过去,而是传首地址。数组名就是数组的首地址。但有两种情况例外:
- sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数组。
- &数组名,取出的是数组的地址。&数组名,数组名表示整个数组。
除此1,2两种情况之外,所有的数组名都表示数组首元素的地址。
6.4 字符串与字符数组
6.4.1 字符串数组的定义与初始化
6.4.2 字符串数组的输入与输出
6.4.2.1 scanf和printf
这时候就像输入输出其他数组一样
6.4.2.2 %s一次性输入输出
6.4.2.3 字符串处理函数gets和puts
6.4.3 字符串处理函数即模拟实现
6.4.3.1 字符串连接函数stract
6.4.3.2 字符串复制函数strcpy
6.4.3.3 字符串长度函数strlen
6.4.3.4 字符串比较函数strcmp
6.4.3.4 模拟实现
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
char*my_strcat(char*dest, const char*src)
{
assert(dest != NULL); //断言,保证dest不为空
char *start = dest;
while (*dest)
dest++;
while (*dest++ = *src++)
;
return start;
}
int main()
{
char arr[20] = "hello ";
char*p = "world!";
char*tmp=my_strcat(arr, p);
printf("%s\n",tmp);
system("pause");
return 0;
}
#include<stdio.h>
#include<stdlib.h>
//3.模拟实现strcpy
//strcpy(str1,st2)将数组str2的值复制到数组str1
void Copy(int* arr1, int* arr2, int len)
{
int i = 0;
printf("arr1=");
for (i = 0; i < len; i++)
{
arr1[i] = arr2[i];
printf("%d ", arr1[i]);
}
printf("\n");
printf("arr2=");
for (i = 0; i < len; i++)
{
printf("%d ", arr2[i]);
}
printf("\n");
}
int main()
{
int arr1[] = { 0 };
int arr2[] = { 1, 2, 3, 4 };
int len = sizeof(arr2) / sizeof(arr2[0]);
Copy(arr1, arr2, len);
system("pause");
return 0;
}
#include<stdio.h>
int my_strlen(char *p)
{
int count = 0;
while (*p)
{
p++;
count++;
}
return count;
}
int main()
{
char arr[] = "asxgjs";
int ret = my_strlen(arr);
printf("%d\n", ret);
system("pause");
return 0;
}
#include<stdio.h>
#include<stdlib.h>
int my_strcmp(const char*buf1,const char*buf2)
{
while ((*buf1 == *buf2) && (*buf1!=0))
{
buf1++;
buf2++;
}
return (*buf1 - *buf2);
}
int main()
{
char *arr= "hsadsjfkl";
char *p = "hsahgjk";
int ret=my_strcmp(arr, p);
printf("%d\n",ret);
system("pause");
return 0;
}
7. 指针
关于指针,博主之前曾经写过相关文章,链接如下:
安得指针千万间,大庇天下地址俱欢颜大家可以点击阅读。在这篇文章里,博主将指针的基础知识介绍的比较详细。
8. 自定义数据类型与链表
在之前我们介绍数组的时候,我们知道数组是一些相同属性的集合。那么,我们怎么把不同的属性会在一起呢?
当我们要描述一个人的时候,我们可以从性别,年龄,身高,体重,学历,性格等等方面来描述他,而这样一个“人”的类型,我们又该如何表示呢?
于是,C语言给出了构造数据类型,结构体类型和共用体类型。
8.1 结构体
8.1.1 结构体声明
struct tag
{
member-list;
}variable-list;
注意末尾的分号一定要有。下面给出一个学生的结构体:
typedef struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}Stu;//分号不能丢
typedef为取别名,而结尾花括号后的Stu是这个结构体的另一个名称,具有与struct stu同样的作用。
8.1.2 结构体成员类型
结构的成员可以是标量、数组、指针,甚至是其他结构体
8.1.3 结构体成员的定义和初始化
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
8.1.4 结构体成员的访问
结构体变量访问成员 结构变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数。
结构体指针访问指向变量的成员 有时候我们得到的不是一个结构体变量,而是指向一个结构体的指针。
struct Stu
{
char name[20];
int age;
};
void print(struct Stu* ps)
{
printf("name = %s age = %d\n", (*ps).name, (*ps).age);
//使用结构体指针访问指向对象的成员
printf("name = %s age = %d\n", ps->name, ps->age);
}
int main()
{
struct Stu s = {"zhangsan", 20};
print(&s);//结构体地址传参
return 0;
}
8.1.5 结构体传参
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
函数传参的时候,参数是需要压栈的。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结构体传参的时候,要传结构体的地址。
8.1.6 结构体对齐
//练习1
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
//练习2
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
//练习3
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
//练习4-结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
为什么存在内存对齐?
- 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址
处取某些特定类型的数据,否则抛出硬件异常。 - 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器
需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
8.2 枚举类型
所谓枚举,就是一一列举。比如一周有七天,我们就把从周一到周日都列举出来,一年有12个月,我们就把1月到12月都列举出来。
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
8.3 联合体
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
8.4 链表
在之前的文章中,博主已经仔细写过链表了,大家可以点击阅读。
链表
后记
好的,这期博文就分享到这里啦。感谢大家支持。这期博文总结了C语言中一些常见的知识点,希望对大家有所帮助。(注:这篇文章中有大量图片来自于由吉根林教授和陈波教授主编的C语言程序设计实践教程)
这篇文章的题目是,以分号结尾的诗。虽然编程看起来是纯逻辑的,纯理性的,但是我依旧觉得,编程也是偏诗性的。编程的思想在于对事物逻辑的追究,而诗歌的思想在于对人本身的追究,我们终究是在不断探索这个世界,当我们创造出许多千奇百怪的东西时,不如说,我们在探索我们千奇百怪的自身。