c语言碎碎念
笔者没有系统学习过c语言,由于要考研,故重新开始学。看课的时候又不想做太过于详细的笔记,故此篇博客只是记录一下我认为有必要记录的琐碎知识方便复习,内容并不全面。重点围绕考研大纲
然后我的专业课的参考书其实是有课后习题的,那一本书的课后习题我会专门开一篇来进行记录。
(一)C 语言程序设计
1、程序设计语言基础
(1)基本数据类型、变量、常量和赋值;
(2)各种运算符和表达式求值;
(3)输入和输出;
(4)选择结构:if语句和switch语句;
(5)循环结构:for循环、while循环和do-while循环。
2、复杂数据类型
(1)数组:一维数组、二维数组;
(2)字符数组;
(3)结构体:结构体变量和结构体数组;
(4)联合体;
(5)枚举类型。
3、模块化程序设计
(1)函数的原型声明、调用及返回;
(2)函数参数;
(3)变量的存储特性。
4、指针及其应用
(1)指针的概念与定义;
(2)指针与数组;
(3)指针与字符串;
(4)指针与结构体;
(5)多级指针;
(6)链表:定义、创建、插入、删除、销毁等操作。
5、文件操作
(1)文件的概念;
(2)文件操作相关的函数功能;
(3)与文件相关的编程方法。
6、综合算法设计
(1)程序设计的常用算法;
(2)程序控制结构的流程图表示,能够用规范的流程图进行算法设计;
(3)利用算法解决和处理实际问题。
第一章
scanf("%d%d",&number1,&number2) //这里就是从键盘接收两个数据,并且讲数据赋值给number1和number2这两个变量, & 是地址运算符,分别获得这三个变量的内存地址。(我才是讲师好吧)😋
printf("sum=%d\n",sum)//这里的%d是按十进值格式输入的数值,后面的\n是换行符,sum的值赋到占位符那里,随后输出。%d的占位符是用给int类型的,double浮点型用的是%lf
//下面是画图需要的两个库
#include<graphics.h>
#include<stdio.h>
#include<conio.h>
initgraph(400,400)//初始化为图形模式,窗口大小为400*400
circle(200,200,75)//以(200,200)为圆心,绘制半径为75的园。
getch()//函数是从输入流中读取一个字符并返回该字符的函数。它是一个非标准函数,通常被用于控制台程序中,用于实现对用户输入的即时响应。
closegraph();//关闭图形模式。
然后我用的教材也是我对应的学校的教材,所以每一章的课后习题是那个教材的。
函数
#include <stdio.h>
int f(int a)
{
return a * a;
}
int main(void)
{
printf("%d\n", f(3));
return 0;
}
这个就是定义了一个函数,计算a的平方,下面就是调用了这个函数f
每一个参数的作用域都是在函数内部(也就是定义的里面,也就是形参。)
其实main函数也可以不用定义返回值,这样就不用写return语句了。也就是main前面的int可以去掉。但是不知道为什么我的vs里面就不能去掉,这个视频说的是可以去掉的也演示了一下
并且运行的顺序也是都是从main函数开始运行的。同时定义的函数是不能写在main函数后面的,必须要写在main前面。但是如果类似如下即可执行:
#include <stdio.h>
int f(int a)
{
return a * a;
}
void pr(); //注意这里的pr后面有分号,意味着扩展函数pr的作用域到此。同时扩展的函数形参是可以省略掉只写上形参的数据类型即可。
int main(void)
{
printf("%d\n", f(3));
pr();
return 0;
}
void pr()
{
printf("xss");
}
同时一个函数的定义是不能放在一个函数体里面,必须是平行的。
if语句
#include <stdio.h>
int main(void)
{
double a, b; //double是浮点型数据类型
printf("请输入2个数:\n");
scanf("%lf%lf",&a,&b); //取地址符
printf("a is %lf\nb is %lf\n", a, b);
if (a >= b)
printf("a是最大值,其值为:%1f\n", a);
else
printf("b是最大值,其值为:%1f\n", b);
//方法2
if(a<b)
a=b; //谁大就赋值到a上
printf("最大值为:%lf",a);
}
//这是一个输出最大值的程序
当多个if语句使用时,elses是属于紧挨着它的上面那个if语句d的内容
当表达式的值为0,表达为假;否则表达式为真。(表达式这个式子返回的结果就是1或者是0,当然其他非零值的表达式结果也为真)
printf和scanf函数
pintf的返回值是整型数据,是输出字符串的个数。
scanf的返回值也是整型数据,是输入变量的个数。
scanf函数的注意事项
1.使用scanf函数输入数据,为什么需要按回车—-回车可以刷新键盘缓冲区内容。键盘缓冲区会有一个换行字符。
2.scanf函数遇到错误输入会停止
3.scanf函数的格式字符串中的字符分类
a.格式声明(格式说明符):%c是字符串,%d是整形数据,%lf是double类型的数据。值得注意的是,一个格式说明符只能存一个单位的变量,对于字符串来说就是一个字母。
b.空白字符(一共有三种,换行,空格和tab(’\n’,’ ‘和’\t’)—-scanf在读操作中忽略掉1个h或d多个空白字符。)
c.非空白字符–scanf在进行读操作的时候剔除掉与这个非空白字符一样的字符
4.对于输入连续的整型或者浮点型数据,可以用空白符起到间隔的数据的作用。但是对于连续的字符型,就不能使用空白符来间隔,
对于第2.条的解释就像这个图片中一样,如果1的scanf出错了,没有输入整型变量,那么这个第一个scanf就会停止,然后这个#就会被下面的第二个scanf函数接收并赋值到a,由此其实也可以推出很多的编程思路,如自动分类变量类型等。
对于这种的scanf,在输入数据之后,按下回车不会立即结束scanf函数,因为后面的\n
会让我们输入的换行符被scanf语句忽略掉,之后输入的所有的空白字符都会被scanf忽略掉,想要停止此scanf函数只有输入错误的数据类型才可以使用其特性停止,但是这时候前面输入的正确的数据其实已经被赋值进相应的内存地址里面了。
对于这种的scanf函数,必须按照其他非空白字符的内容一起输入才能正确赋值a变量,要不然则会让scanf函数停止,a就只能保持垃圾值的状态。
运算符
概念学习:
1.理解概念:优先级,操作数,结合方向—初等运算符优先等级为1(最高等级的优先级),单目运算符优先等级为2.
比如说
1+2 这个+就是双目
&a 这个&就是单目
同时括号就是初等运算符之一
2.只有单目运算符和赋值运算符的结合方向是自左往右的。
(12条消息) C语言运算符优先级列表(超详细)_c优先级_对望小秘的博客-CSDN博客
算数运算符
1.出好运算符的操作数如果都为整型,那么其结果也为整型。会省略商后面的小数部分。如果操作数中有一个小数,那么输出的结果也是小数。
2.把浮点型数据赋值给整型变量的时候,会丢失掉小数部分,只取整数部分。
3.取余符号%
两边操作数都不能是浮点型数据。
4.赋值表达式的值是其左操作数最终的值,这个值为一个左值。
运算符2
1.逗号表达式的值,是其最右i表达式的值。(逗号运算符的优先级是15级,是最低的)
2.在c语言中,优先级并不完全决定运算顺序。
3.只有运算符(&& || 逗号 条件运算符)规定了运算顺序,必须自左往右,即使有括号也会先执行左边的语句。
4.&&和||都先计算左边表达式的值,如果左边表达式的值能够确定整个表达式的值,那么右边表达式的值就不会被计算。
4.操作数的求值运算,不同的编译器可能有所不同—不要依赖任何不可移植操作。(类似下面这串代码)
((a += 1) + (a *= 2)) //两边的表达式不知道先计算那一边
运算符3-条件运算符
1.条件运算符是c语言中唯一的三目运算符,其优先级为13
2.c语言中条件运算符的运算方向也必须自左往右的
下面举几个例子
1?printf("哈哈\n"):printf("呵呵\n");
//输出哈哈,就是类似于ifesle语句,为真就执行前面那个表达式,为假就执行后面那个表达式。
这些例子都需要好好看,我相信大家能理解,而且这些例子我认为肯定会在题目上出的。
全局变量及宏定义
1.宏名,全局变量名建议大写
2.#开头的都是预处理指令,预处理是发生在预编译阶段(编译阶段之前),对源程序文件进行一些简单的文本替换–VC++中编译按钮集成了预编译和编译的
3.全局变量的作用域就是从定义处开始,到整个文件末尾。
其中未初始化的全局变量,系统就给他默认初始化为0。(全局变量就是除了形参变量之外所有函数体之外定义的变量)
杂项
&&逻辑与
||逻辑或
printf里面嵌套函数可以输出函数的返回值
if else语句也可以循环嵌套。
c语言中的关键字
标识符的命名规范
1.标识符只能由字母、数字、下划线构成。而且必须是字母或者是下划线开头。
2.自己定义的标识符不能和c语言的关键字相同
3.不同的c语言编译器c语言标识符的长度标准不同
4.同一个“花括号”中,不能直接定义重名变量。
5.在文件的某处引用重名变量时,所引用的变量就是作用域较小的那一个变量。
第二章 算法
算法的基本概念
一、一个程序主要包含的2方面信息
1.对数据的描述,在程序中要指定用到哪些数据以及这些数据的类型和数据的组织形式。这就是数据结构
2.对操作的描述。即要求计算机进行操作的步骤,也就是算法。
二、沃斯提出的一个公式 算法+数据结构=程序
三、计算机算法可各位2大类别:数值运算算法和非数值运算算法
四、算法的特性:1.有穷性2.确定性3.有零个或多个输入4.有一个或者多个输出5.有效性
循环语句
while语句
#include <stdio.h>
int main(void)
{
int i = 1;
while (i <= 10)
{
printf("%d\n", i);
i = i + 1;
}
return 0;
}
上面这个就是一个while的循环语句,当while条件括号里面的表达式是真的时候,就会执行花括号里面的内容,根据这个东西,我们就可以推断,while括号里面的内容,是必定会执行一遍的。
所以如果while里面的是一个表达式,那么就可能造成危害,如下
(不知不觉就开始了捏)
回到正题
for语句
#include <stdio.h>
int main(void)
{
for (int i = 1; i <= 100; i = i + 1)
{
printf("%d\n",i);
}
return 0;
}
if语句里面有三个语句,第一个是初始化值,中间那个是设置条件,第三个是执行完花括号之后会执行的语句。
第二个条件成立的时候,不会执行第三个条件而是直接跳转到花括号里面,等花括号执行完毕之后,才会执行第三个语句。
同时,如果第二个条件不成立的时候,也不会执行第三个语句,而是直接跳转到花括号外面继续执行。
i++与++i
这两个东西的区别已经是老生常谈了。
大概理解就是,需要调用i++或者++i的时候,把他看作是一个变量,++i就是先增加再调用,i++就是先调用再增加。下面写一个例子来形象的说明一下这个问题。
#include <stdio.h>
int main(void)
{
int i = 0;
printf("%d\n", i++);
printf("%d\n", i);
i = 0;
printf("%d\n", ++i);
printf("%d\n", i);
}
这是以上程序的执行结果
扩展一个难题(大概吧)
#include <stdio.h>
int main() {
int x = 0;
printf("%d,%d\n", x, x++);
printf("%d", x);
return 0;
}
printf(“%d,%d\n”, x, x++)
函数,从右向左计算,然后从左向右输出。
就像下面一样,首先左边的%d获取最右边的x++的值,然后第二个%d获得最左边的i(此时已经执行过i++)的值
当然!!!!!最重要的是在不同的编译情况下printf的运算规则是不一样的,我看的教学视屏里面是从右往左,但是我的不是,我的是vs。
算法的表示法
1.自然语言表示法
2.用流程图表示
下面是一些美国国家标准协会规定的一些常用的流程图符号。
举个例子
上面这个流程图就是计算5的阶乘。下面是三中基本结构
然后呢,流程图的主要弊端就是,对流程线的使用没有严格的限制,使用者可以不受限制的使流程随意的转来转去。这种乱麻一样的流程图算法称之为BS算法。
为此我们引入了N-S流程图
N-S流程图表示法
这个是求5的阶乘的N-S流程图
算法练习:求一个素数
素数:除1以外只能被1和它本身整除掉自然数
#include <stdio.h>
int main() {
int input = 0;
printf("请输入一个自然数:");
scanf("%d", &input);
int i = input - 1;
if (i == 0 || i == -1) {
printf("1是素数但是0不是素数哦");
return 0;
}
else if (i < -1) {
printf("老子喊你输入自然数你是听不到是吧?");
return 0;
}else {
while (i != 1) {
if (input % i == 0) {
printf("%d", i);
printf("这个数不是素数哦!");
return 0;
}
i--;
}
printf("这个数是素数哦!");
return 0;
}
}
这样就是一个算法了,当然这个是我自己写的哈。还有其他的练习题,可以自行搜索。
第三章 顺序程序设计
c语言中常用的占位符
%d 整型int
%ld 长整型long (int)//long是long int的简写方式
%lld 长长整型long long (int)
%hd 短整型short int
%u 无符号整型unsigned int
%hu 无符号短整型unsigned short int
%lu 无符号长整形unsigned long int
%llu 无符号长长整型unsigned long long
%f 浮点型float
double比较特殊,它的输入占位符是%lf,输出只能是%f
%e(E) 以指数形式表示的浮点型
%m.nf 可控制输出小数位数
//浮点型也有长短型,可以参考整型
%c 字符型char
%s 字符串
%o 以八进制输出
%x 以16进制输出
%p 变量地址
%i 结构体输出
常量
常量分类:
1.字面常量(直接常量):数值常量(分为整型常量和浮点型常量),字符串常量和字符常量。
2.符号常量
3.常变量
#include <stdio.h>
int main() {
//1.整型常量
printf("%d\n",234);
//2.浮点型常量
printf("%lf,%lf\n", 3.24, 3e-2); //3e-2代表3的10的-2次方 (就是3×10^-2)
//3.字符串常量
printf("%s", "%dabc\n"); //字符串使用的格式说明符为%s,输出%dabc
printf("%%dabc\n"); //想在printf的格式字符串中输出"百分号",需要再加一个"百分号"。 输出%abc
//4.字符型常量 字符也是以整型方式存储在内存中
printf("%c,%d\n",'a','a'); //普通字符常量,输出 a,97
}
上面是输出,下面是补充的一些转义字符。转义字符中,\r是将当前位置滚回到本行开头。
下面这五个字符对应的整型数据必须背过
字面常量讲完了,现在说说符号常量和常变量
符号常量:只存在于预编译阶段,编译阶段就不存在了,经过预编译后,全部变为字面常量。如下
#define PI 3.14
#define ID 101
常变量: C99中新增加的内容
const int a = 45; //常变量必须在定义的时候初始化,初始化之后其值不可以被修改(类似php里面那个)
整型
补码
1.补码:整型以补码的形式存放
(1)一个正数的补码是此数的二进制形式。
求一个正整数的二进制:除以2,直至商为0,余数倒数排序。
(2)一个负数的补码,先求其绝对值的补码,然后该补码所有位取反,所得之数最后加1
(3)0的补码,所有位全都是0
注意:在存放整型的存储单元中最左边一位是用来b表示符号的(正整数最左边是0,负数是1)
n位2进制补码能代表的整数范围是-2^(n-1)~2^(n-1)
整型数据的存储空间及范围
有符号的前面只要加一个unsigned就是无符号得了,只能存放自然数。当然有了字节数量,我们就可以通过这个算出他能表示出的整数范围。用上面补码的那个公式。(一个字节等于8位)
扩展
sizeof
sizeof()//这个东西不是函数,他是一个单目运算符,他可以求一个数据类型或者数值所占到字节数。其实不用那个圆括号也可以,但是最好或者说是必须加上,避免后续出错。
整型数据输出10进制数
%u是指的就是无符号整型数据。
整数数据输出8进制或16进制
%o就是8进制,剩下的x就都是16进制,其中的x如果是小写的其输出的结果如果带字母的话也是小写的。
格式说明符表格
隐式数据转换
分析一下上面这个代码,-1
是一个int类型的数据,占了4个字节,也就是32个二进制位。而-1
的补码就是32个1(一个负数的补码,先求其绝对值的补码,然后该补码所有位取反,所得之数最后加1),前面16个1后面16个1。而%hu是短的无符号整型,只占16个2进制位。当占用字节大的赋值给小的时候,只会采取后面16个1。而16个1的二进制对应的10进制就是65535
上面是大字节赋值给短字节,下面我讲讲短的赋值给长的,简单的讲就是把短的全部赋值到长的低几位,长的高几位填0.(下图中的us等于16位的全是1也就是65535)
现在讲讲占用字节数较短的有符号的数赋值给占用字节较长的数会怎么样,先把“短的”全部内容复制到“长的”低几位,如果“短的”是自然数,“长的”高几位填0,否则,“长的”高几位全部填1.
当然如果等长的话,就会保留原有的补码(补码是重点哦,整数类型的都是通过补码的形式存储在变量里面的。)
强制类型转换
字符型
字符是以整数形式(ASCII码)存放在内存单元中。ASCII共128个字符(也就是说最多用7位就可以表示)
下面是一些需要记住的对应字符的ascii编码
值得注意的是一个小写字母比大写字母的asccii码大32
字符型数据的存储空间和值
字符型数据分为两中类型。signed char
和unsigned char
也就是有符号的和无符号的类型。
他们两个都是占一个字节。
其中signed char
的取值范围是-128~127
,而unsigned char
的取值范围是0~255
.
需要注意两个点:
1:C99把字符型数据作为整型类型的一种。
2.在使用有符号字符型变量时,允许存储的值为负数,但是字符的代码不可能是负值,所以在存储字符时只用到了0~127
这一部分。
如何定义字符型变量
#include <stdio.h>
int main() {
signed char sc;
unsigned char uc;
char ch; //在vc++中,char类型是signed char类型的,在其他编译器中也有可能是无符号的char类型
printf("%d,%d,%d\n", sizeof(sc), sizeof(uc), sizeof(ch));//输出1,1,1
//分析下段代码的运行结果
uc = -1;
ch = 255;
printf("%d,%d\n", ch, uc); //输出-1,255
//就相当于定义了一个int变量,输出了这个int变量而已。因为在c99中,字符型变量算作整型数据的一种。
}
getchar和putchar函数
getchar和putchar函数包含在stdio.h头文件中。
getchar()
函数,返回类型为int型,返回值为用户输入的ascii码,出错返回-1.
putchar()
向屏幕输出一个字符。
浮点型
浮点型数据包括:单精度浮点型,双精度浮点型,长双精度浮点型,复数浮点型。主要讲前面三中。
浮点型数据是以规范化的指数形式存放在内存当中,把小数不封中小数点前的数为0,小数点后第一位数不为0。
一些小tip
其中值得注意的是:
当把一个长的浮点型数据赋给一个较短的浮点型b变量时,可能会造成精度的损失。而且在有的编译器中会出现告警。
printf用到的格式附加字符
如上图,其中d前面的5就是域宽,输出的内容就是5个字符
可以看到是向右对齐的,我们也可以向左对齐。
看这个,输出的字符占7,然后小数点后面占2个。如果是7.0
和7.
那么就不会输出小数位。
scanf函数的注意事项
因为scanf里面有5,也就是域宽,只取了5位数。
后面输入的678就被后面的getchar()给接收了
上面这张图,scanf里面有个-5
,可以看到并没有接收到我们输入的内容,全部传入到getchar里面了。由此可见,scaf不能使用负数的附加字符
这个也是一样的,用了小数的附加格式字符,同样没有接收到数据,所以由此可见,scaf也不能使用小数点附加字符。
总结一下
scanf函数不能使用#,-,m.n
的格式附加字符。但是scanf可以使用域宽的格式附加字符
运算符
自增自减不能用于一个数值,只能用于一个变量。
i+++j //这个代表的是(i++)+j
//但是其他编译器也有可能不是这个结果
//对于这种不可移植的操作我们应该尽量避免
对于上面这张图,(double)的含义是将a对数值转化成double,而不是将a这个变量变成double。也就是这个表达式是double,但是a还是原来的a,依然是int。
第四章 选择结构程序设计
判断2个实型数据是否相等
//我们这里不用==来,用了一个扩展的函数,不在stdio.h里面,所以我们需要从新导入一个库
#include <stdio.h>
#include<math.h>
int main() {
float a = 10.2222225, b = 2222229;
if (fabs(a - b) <= 1e-7) //这个函数的作用就是求绝对值,他是在math.h库里面的
printf("这两个数相等");
else
printf("这两个数不相等");
}
这两个分别是求double和int类型的绝对值。
逻辑型变量
其实也就是布尔型的数据类型,同时在c语言里面,布尔型也是整型数据的一种。
<studbool.h>
这个文件中有_Bool
这个数据类型,_Bool
与bool是同义词,vc++中没有这个头文件
同时布尔型变量也是c99中新增的数据类型,用他所定义的变量,可以用来表示真假,取值范围为1和0.
bool型的数据变量只占一个字节。
switch语句
这里我们使用一个”电梯程序”来做示范。
#include <stdio.h>
int main() {
printf("请输入想到达的楼层:");
int a;
scanf("%d", &a);
switch (a) {
case 1:
printf("上1楼\n");
break;
case 2:
printf("上2楼\n");
break;
case 3:
printf("上3楼\n");
break;//这个break就会停止,要不然会一直执行下去哈
//这里case后面跟常量的必须是一个整型类型
//每一个case后面的内容都不相同
default:
printf("没有这个楼层");//这个就是默认嘛,没有情况的时候就用这个
}
}
条件编译
这里使用一个例子来举例子
#include <stdio.h>
int main(void) {
char ch;
while ((ch = getchar()) != '\n')
{
#if 1
if (ch >= 'A' && ch <= 'Z')
ch += 32;
#else
if (ch >= 'a' && ch <= 'z')
ch -= 32;
#endif
putchar(ch);
}
return 0;
}
这个就是把大写字母转成小写字母,小写字母转大写字母。条件编译呢就是那个#后面的东西,如果if后面跟的表达式为真就编译后面的字符,如果是假就不编译第一个,转而编译第二个。
总而言之,条件编译的功能就是,当程序满足某个条件的时候,才编译某段代码。使用条件编译的时候,要么使用一个数值来作为条件,要么用一个宏定义来作为条件,也就是说是不能用变量来做条件编译的条件的。
头文件
文件包含指令:
1.#include<文件名> 系统到存放c库函数头文件的目录中寻找要包含的文件,这称之为标准方式
2.#include”文件名” 系统先在用户当前目录中寻找要包含的文件,若找不到,再按标准方式查找。
注意:头文件后缀可以是.h
.c
或者是没有头文件
当然值得注意的是,包含两次同一个文件,就会执行两次函数,也就是说有可能包含的时候,条件就有可能出现变化。
第五章 循环结构程序设计
do…while语句
一般形式
do{
语句}
while(表达式);
//首先执行do,然后再判断,真就继续,假就退出
break的作用
用来中指他所在的最内层的一个循环或者switch语句。
continue
结束当前单词循环,但是整个大循环不结束(跳转到最内层的一个循环),举个例子。
同时只能在循环语句中使用这个continue。
输出不能被三整除的数字。
#include <stdio.h>
int main(void) {
for (int i = 1; i <= 100; ++i)
{
if (i % 3 == 0)
continue;
printf("%d\n",i);
}
}
几个小练习
#include <stdio.h>
//输出一个如下的矩阵
int main(void) {
for (int i = 1; i <= 5; ++i)
{
for (int j = 1; j <= 5; ++j)
{
printf("%5d", i * j);
}
printf("\n");
}
}
第六章 数组
单位换算
内存地址
以2GB内存地址为例,2GB = 2^31B,计算机就为这2^31个存储单元(字节)的位置编码:
地址编码对应的格式声明可以是%p %o %x
举个例子吧,这个东西可以输出整型变量a的内存地址
#include <stdio.h>
int main(void) {
int a;
printf("%p", &a);
}
下面是定义数组的例子
#include <stdio.h>
int main(void) {
int d[3];
d[0] = 1; d[1] = 2; d[2] = 3;
printf("%d,%d,%d\n",d[0],d[1],d[2]);//输出内容
printf("%d,%d,%d\n", &d[0], &d[1], &d[2]);//输出内存地址
}
一个整型变量是4个字节,通过观察内存地址,你可以看到,每一个内存地址相差的位数是4,也就是说数组的每一个元素的内存地址是紧挨着的。
数组
#include <stdio.h>
int main(void) {
int a[2*2];
int b = 2;
int c[2 * b]; //这就是错误的
return 0;
}
定义数组的方括号中,只能使用整型常量的表达式,不能带入变量。
#include <stdio.h>
int main(void) {
int a[2*2];
const int b = 2;
int c[2 * b];
return 0;
}
而上面这个就是正确的,因为这时候的b已经是一个常量了。
同时方括号中也不能是小数,只能是整型数据,当然使用(int)
来转化为整型也是可以的。而且之前不是有那么多的看为整型数据的数据类型吗,比如说:布尔类型,字符型
这些都是可以用来定义的。
定义并初始化数组
如果一个数组没有被初始化,那么他里面的东西都是垃圾值。下面展示一下初始化的方法。
#include <stdio.h>
int main(void) {
int a[4] = {1,2,3,4};
for (int i = 0; i < 4; ++i) {
printf(" % d", a[i]);
}
return 0;
}
后面的花括号就是指定的里面的值,这种方式我们称之为完全初始化,也就是定义了每一个值。同时也可以这样
#include <stdio.h>
int main(void) {
int a[] = {1,2,3,4};//这里没有指定数组里面元素的个数
//所以计算机会根据你赋予的值来确定数组中元素的个数.
for (int i = 0; i < 4; ++i) {
printf(" % d", a[i]);
}
return 0;
}
上面两个都是完全初始化,下面介绍一下非完全初始化
#include <stdio.h>
int main(void) {
int a[4] = {1};
for (int i = 0; i < 4; ++i) {
printf(" % d", a[i]);
}
return 0;
}
这里就是非完全初始化,其他没有赋值的元素的默认值就是0
数组排序-冒泡
哈哈哈又到了经典的排序环节了哈。我估摸着就是冒泡排序吧,稍微看看,要是懂了的话,就写个例子差不多了。
#include <stdio.h>
int main(void) {
//排序五个数
int arr[5];
for (int i = 0; i < 5; ++i) {
scanf("%d",&arr[i]);
}//上面就是循环接收五个数,
//排序部分
for (int k = 0; k < 4; ++k) {//n个数排序只需要循环冒泡n-1次
for (int i = 0; i < 4 - k; ++i) {
//这里的4-k意思就是第一次最大的拍到最后
//第二次第二大的拍到倒数第二
int b = arr[i];//b是中间变量,方便我们交换数据
if (arr[i] > arr[i + 1]) {//完成交换数据
b = arr[i+1];
arr[i + 1]=arr[i];
arr[i] = b;
}
}
}
printf("\n\n继续输出结果\n");
//输出部分
for (int i = 0; i < 5; ++i) {
printf("%d\n", arr[i]);
}
return 0;
}
接下来我大致讲讲冒泡排序吧,大概就是数组中,相邻的两个元素不断比较,大的那个放在右边,然后一直比较到倒数第二个,这样最大的值就被放在最右边了。
然后继续比较这次比较到倒数第三个,第二大的元素就被放在倒数第二个了。也就是说,比较n个数据,只需要循环n-1次冒泡排序。
某种意义上讲,直接不减k也是可以的,只不过会增加时间复杂度。
数组排序-选择排序
幸好没跳过,原来还有个选择排序。
选择排序也是和冒泡排序是一样的,都是将最大的值放在最后一个,只不过选择排序是直接交换元素的位置到指定的位置。
举个例子,最大的数放在最右边,第二大的数直接放在倒数第二个。
下面我们写一个程序例子。
#include <stdio.h>
int main(void) {
//排序五个数
int arr[5];
int len = 5;
for (int i = 0; i < len; ++i) {
scanf("%d",&arr[i]);
}//上面就是循环接收五个数,
//排序部分
int c = 0;
for (int i = 1; i < len; ++i) {
if(arr[c] < arr[i]){
c = i;
}
}
int buf = arr[c];
arr[c] = arr[len - 1];
arr[len - 1] = buf;
printf("\n\n继续输出结果\n");
//输出部分
for (int i = 0; i < 5; ++i) {
printf("%d\n", arr[i]);
}
return 0;
}
这个只是完成了一次排序,也就是把最大值放到最右边去了,现在执行n-1次循环即可实现选择排序,相比这个,我还是更喜欢冒泡排序哈
#include <stdio.h>
int main(void) {
// 排序五个数
int arr[5];
int len = 5;
// 循环接收五个数
for (int i = 0; i < len; ++i) {
scanf("%d", &arr[i]);
}
// 排序部分-选择排序
for (int i = 0; i < len - 1; ++i) {
int max_index = 0;
for (int j = 1; j < len - i; ++j) {
if (arr[j] > arr[max_index]) {
max_index = j;
}
}
int temp = arr[max_index];
arr[max_index] = arr[len - i - 1];
arr[len - i - 1] = temp;
}
printf("\n\n继续输出结果\n");
// 输出部分
for (int i = 0; i < len; ++i) {
printf("%d\n", arr[i]);
}
return 0;
}
现在直接在上面那个的基础上套了一次循环,完成。
指针-简单入门
指针就是一个变量的地址,我们可以通过这个地址来访问这个变量。
#include <stdio.h>
int main(void) {
int* p;
int a = 2;
p = &a;
*p = 3;
printf("%d", a);
return 0;
}
我们通过*p来访问了a变量的内存地址,将其赋值为3.
现在我们再继续写一个例子来帮助我们理解
#include <stdio.h>
int main(void) {
int * p;
int arr[3] = {1,2,3};
p = arr;
*p = 10;
*(p+1) = 20;
*(p+2) = 30;
for (int i = 0; i < 3; ++i) {
printf("%5d", arr[i]);
}
return 0;
}
首先需要说明两点,第一点就是p被创造是一个指针对象的时候,然后赋值的时候,就是p=arr
那里,p的值当时是这个数组的第一个元素的内存地址。
第二点就是明明p只加了个1为什么能操作第二个和第三个元素呢?原因就是其实p加的是1*sizeof(*p)
,因为*p又是一个int类型的数据,所以他的字节数就是4。
下面继续举个例子
#include <stdio.h>
int main(void) {
int * p;
int arr[3] = {1,2,3};
p = arr;
//*(p+2) => p[2] => 2[p]这三个东西是等价的
p[2] = 300;
p[1] = 200;
0[p] = 100;
for (int i = 0; i < 3; ++i) {
printf("%5d", arr[i]);
}
return 0;
}
数组的增删查改及倒置
增删查改
直接上例子
#include <stdio.h>
#define LEN 10
//定义了一个函数,这个函数可以看到这个数组的有效元素的个数
int length(int* a) {//或者写成int a[LEN]
int j = 0;
while (j < LEN) {
if (a[j] == 0)
break;
else
++j;//虽然j是索引,但是最后还是会多执行一个++j所以就变成了长度
}
return j;
}
void show(int a[]) {//输出这个数组的所有元素
int len = length(a);
printf("数组中的元素是");
for (int i = 0; i < len; ++i) {
printf("%5d",a[i]);
}
putchar('\n');
}
bool del(int* a, int index) {//定一个函数,删除对应index的元素
int len = length(a);
if (len == 0 || index > len - 1)//看index是否超出长度或者len是不是0
return false;
else//具体要怎么删除呢,大概就是整体前移,然后最后一个赋值为0
{
for (int i = index + 1; i < len; ++i) {
a[i - 1] = a[i];
}
a[len - 1] = 0;
return true;
}
}
bool insert(int* a, int index, int value) {//定义一个函数用来插入变量
int len = length(a);
if (len == LEN || index<0 || index>len) {//检查索引超过没有,检查len是不是最大值,检查index是不是小于0
return false;
}
else {
for (int i = len - 1; i >= index; --i) {//就是每个元素往后面移动
a[i + 1] = a[i];
}
//最后再在index那里赋值元素
a[index] = value;
return true;
}
}
int main(void) {
int a[LEN] = { 3,6,9,12,15 };
printf("长度是:%d\n",length(a));
show(a);
if (del(a, 4))
{
show(a);
}
else
{
printf("删除失败\n");
}
if (insert(a, 2, 8)) {//下标为2的地方给他插入一个8的元素
printf("插入成功");
show(a);
}
else {
printf("插入失败");
}
return 0;
}
成功输出。
倒置
就是定义一个函数嘛,然后第一个和最后一个交换,倒数第二个和第二个交换,直到没有数组交换位置。
直接上例子
#include <stdio.h>
#define LEN 10
//定义了一个函数,这个函数可以看到这个数组的有效元素的个数
int length(int* a) {//或者写成int a[LEN]
int j = 0;
while (j < LEN) {
if (a[j] == 0)
break;
else
++j;//虽然j是索引,但是最后还是会多执行一个++j所以就变成了长度
}
return j;
}
void show(int a[]) {//输出这个数组的所有元素
int len = length(a);
printf("数组中的元素是");
for (int i = 0; i < len; ++i) {
printf("%5d", a[i]);
}
putchar('\n');
}
int main(void) {
//写一个倒置数组的东西
int arr[LEN] = { 3,6,9,12,15 };
show(arr);
int len = length(arr);
int from = 0;
int end = len - 1;
int buf;
while (from < end) {
buf = arr[from];
arr[from] = arr[end];
arr[end] = buf;
++from;
--end;
}
show(arr);
return 0;
}
查找位置
就是找到一个元素的索引
大概思路就是,一直用一个j来for循环,当arr[j]==value
的时候,就输出这个就可以了,现在给出一个例子。
当然查找的时候也可以使用二分法查找
#include <stdio.h>
#define LEN 10
void bubbleSort(int* a) {//冒泡排序
for (int i = 0; i < LEN - 1; ++i) {
for (int j = 0; j < LEN - i - 1; ++j) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
int binarySearch(int* a, int target) {//二分法查找
int low = 0;
int high = LEN - 1;
while (low <= high) {
int mid = (low + high) / 2;
if (a[mid] == target)
return mid;
else if (a[mid] < target)
low = mid + 1;
else
high = mid - 1;
}
return -1; // 如果目标元素不存在于数组中,返回 -1
}
int length(int* a) {
int j = 0;
while (j < LEN) {
if (a[j] == 0)
break;
else
++j;
}
return j;
}
void show(int a[]) {
int len = length(a);
printf("数组中的元素是");
for (int i = 0; i < len; ++i) {
printf("%5d", a[i]);
}
putchar('\n');
}
int main(void) {
int arr[LEN] = { 6, 3, 9, 2, 5, 1, 8, 4, 7, 0 };
bubbleSort(arr); // 对数组进行冒泡排序
show(arr);
int target = 5;
int index = binarySearch(arr, target); // 在排序后的数组中查找元素的索引
if (index != -1)
printf("元素 %d 的索引是 %d\n", target, index);
else
printf("元素 %d 不存在于数组中\n", target);
return 0;
}
二维数组定义时的初始化
二维数组定义的一般形式
//类型说明符 数组名 [常量表达式][常量表达式]
他的内存存放是如下
实际上都是一维数组,只是表达形式是数组。下面举个例子
#include <stdio.h>
int main(void) {
int a[3][4] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
//等价于int a[3][4] = { {1,2,3,4},{5,6,7,8},{9,10,11,12} };
//输出方式1
for (int i = 0; i < 3; ++i) {
for (int j=0; j < 4; ++j) {
printf("%4d", a[i][j]);
}
}
printf("\n以上是输出方式1\n");
//输出方式2,将数组当做一维数组
for (int i = 0; i < 12; ++i) {
printf("%4d", a[i / 4][i % 4]);
}
printf("\n以上是第二种输出方式\n");
return 0;
}
同样的,二维数组的非完全初始化,对于没有赋值的元素,默认值就是0
指针引用二维数组
同时我们也可以通过指针来引用多位数组,下面这个是用指针引用一维数组的三个方法
理解一下,a其实就代表的是a数组中第一个元素的内存地址,*就是指针来引用他们。&
代表被指,星号代表指人
现在讲讲通过指针引用多维数组,理解一下,多维数组其实就可以看成是数组的数组,举个例子。a[1][2]
我们就可以把a[1]
看成是数组名字,后面的2是索引。
&a[1] 是一个指向行的地址,值为1012
a[1]+2 是一个指向int的指针,值为1020
*(a+0)+1 是一个指向int的指针,值为1004
&a[1][1] 是一个指向int的指针,值为1016
*(*(a+1)+2) 是一个指向int的指针,值为最后一个元素的地址,也就是1020
这里放个友链啊,其实指针数组的东西后面我们会提到,但是现在有人给我讲了那我也就放出来。
此人也是嵌入式或者硬件方面的大佬呢
字符数组
小tips:\0
空字符是c语言中字符串结束的标志。
这两个是字符型数组定义是初始化所特有的形式。
同时输出字符数组,可以直接
printf("%s",数组名);
不用循环遍历。
而且还有个小tips,字符串常量在计算机的内存当中是一个没有名字的数组。
字符串的输入和输出
scanf
对于一个输入项,他是一个字符数组的数组名,就不需要取地址符。如下所示
#include <stdio.h>
int main(void) {
char a[30], b[30], c[30], ch;
scanf("%s%s%s%c", a, b, c, &ch);
printf("%s%s%s%c", a, b, c, ch);
return 0;
}
中间的空格就分开了三个单词,将三个单词分别存放在了abc数组里面,最后的换行符\n
被存放在了变量ch里面。
使用scanf函数输入多个字符的时候,可以用空格或者tab来进行分割。
而对于上面那个程序,数组中没有被赋值的元素,是一个垃圾值。
getchar和putchar
#include <stdio.h>
int main(void) {
char a[30], b[30], c[30], ch;
gets(a);
ch = getchar();
puts(a);//puts在输出之后,会再输出一个换行符。
putchar(ch);
return 0;
}
//gets函数和scanf的区别就是,gets函数会把空格和tab值也赋值给a数组中的元素,并且忽略掉换行符。
//printf和scanf被称为k可变格式s输出输入函数
处理字符串的函数
返回字符串长度的一个函数,编写如下
#include <stdio.h>
int strlen(char* arr) {
if (arr == NULL)
return 0;
int len = 0;
while (arr[len] != '\0')//应为\0空字符是结尾
++len;
return len;
}
int main(void) {
char a[] = "hahaha";//虽然最后那个空字符串也要算一个字符,但是并不算一个有效字符。
printf("%d\n", strlen(a));//输出6
return 0;
}
tm这里直接给我听睡着了
就是些自己写的函数,么意思。以后我们有习题了自己慢慢学。
第七章 函数
宏定义
直接给例子
同时在同一个文件中可以使用重名的宏名,
#define A
******
*****
******
#define A
######
####
//第一个宏名A作用于****
//第二个A作用于####
undef
#define A
******
*****
******
#undef A
######
####
//第一个宏名A作用于****
//而undef之后,A就不被定义了
宏名与函数重名
#include <stdio.h>
void F(int a)
{
printf("%d", a);
}
#define F(a) F(a);printf("haha")
int main(void) {
F(3);
return 0;
}
//输出3haha
看上述程序,我们分析一下。每个程序执行的时候都是先执行预处理指令,首先会include
然后执行define
执行完define
之后,F(a)
在define
的作用域中就已经变成F(a);printf("haha")
,随后这个define
再执行完之后就消失了。随后执行的F(a);printf("haha")
中的F(3)
就是最开始定义的函数了。所以输出3haha。
注意上面这团代码的函数定义是在define的作用域之外的,如果在define的作用域之内,define定义的宏同样会作用在void函数的那一行。
函数的指针
#include <stdio.h>
int max(int a, int b) {
return a > b ? a : b;
}
int min(int a, int b) {
return a > b ? b : a;
}
int main(void) {
int (*p)(int, int);//定义了一个函数指针,p可以用来指向函数类型为int,并且两个参数为int的函数
p = max;//p指向了max函数
printf("%d\n", (*p)(10, 5));
return 0;
}
自动变量和寄存变量
#include <stdio.h>
int main(void) {
auto a = 3;
printf("%d\n", a);
register int j = 2;
printf("%d", j);
return 0;
}
寄存器变量register,当我们的某个数被频繁调用的时候就可以使用Register修饰变量,因为在寄存器中调用的时候很快。但是大多数编译器在编译的时候如果检测到有变量被频繁调用,那么就会自动的将这个变量转换为register变量。所以其实没什么用。
extern的用法
用法1:
extern可以在本文件内扩展一个全局变量的作用域,extern只能修饰全局变量。如下所示
#include <stdio.h>
extern int A;
int main(void) {
printf("%d\n", A);
return 0;
}
int A = 10;
用法2:
将一个全局变量的作用域扩展到其他文件。
如下
文件1:
#include <stdio.h>
extern int A;
int main(void) {
printf("%d\n", A);
return 0;
}
文件2:
#include <stdio.h>
int A = 3;
最后执行就会输出3
值得注意点是,全局变量在不同文件中是不可以有重名的。
static
用static修饰的全局变量称作静态外部变量,其他文件不可以通过它的变量名来引用该变量。
static int B = 100;
虽然不能直接用变量名来引用B,但是可以通过指针的方式变相引用如下
文件1:
#include <stdio.h>
extern int B;
extern int* P;
int main(void) {
printf("%d\n", *P);
return 0;
}
文件2:
#include <stdio.h>
static int B = 5;
int* P = &B;
通过这种方式就可以变相的引用静态变量。
静态局部变量和其他类型变量的区别
如果定义静态局部变量的时候没有初始化,它将有一个默认值0.举个例子
#include <stdio.h>
void f(void) {
int i = 10;//每一次调用f函数的时候,i都会被重新赋值
static int j;//没有初始化, 所有有默认值,这个默认值不断的被++
printf("%d,%d\n", i++, j++);
}
int main(void) {
f();
f();
f();
return 0;
}
从上面这个例子可以看出,静态局部变量是在编译的时候赋初值。即只赋初值一次,不会随函数调用结束后其存储单元消失
而自动变量是在函数调用的时候,赋初值,调用一次赋值一次,调用结束后其存储单元就消失.
当然在函数声明的外面,是不能够直接引用j变量的,因为j的作用域不在这里,但是同样的,我们可以使用指针来间接的引用j变量.如下
#include <stdio.h>
int* P;
void f(void) {
int i = 10;//
static int j;
printf("%d,%d\n", i++, j++);
P = &j;
}
int main(void) {
f();
f();
f();
printf("%d\n", *P);//通过指针来引用
return 0;
}
内部函数和外部函数
在定义函数的时候在函数的首部加extern代表这个函数是外部函数,extern其实可以省略不写。
外部函数的特点就是,在其他文件中,直接使用函数名就可以引用他。
当然如果要在另外一个文件调用外部函数的时候,需要在这个文件的首部用extern声明一下这个函数。(当然这个extern也是可以省略的)举个例子
文件1:
#include <stdio.h>
extern void f();
int main(void) {
f();
return 0;
}
文件2:
#include <stdio.h>
void f(void) {
printf("我是外部函数");
}
这样就会输出我是外部函数,文件1调用了文件2的函数。
现在说说,内部函数,定义一个内部函数需要在函数的首部加上static,这样的话在其他文件就不能直接用函数名调用函数了。当然还是能通过指针间接的调用这个static函数。下面举个例子
文件1:
#include <stdio.h>
extern void(*P)(void);
int main(void) {
(*P)();
return 0;
}
文件2:
#include <stdio.h>
static void f(void) {
printf("我是用指针调用的内部函数函数");
}
extern void(*P)(void)=f;//定义了一个指向f的全局指针
下面写几个值得注意的点:
1.在不同文件中可以有重名的外部函数吗?
答案是不可以
2.系统如何处理函数的声明
函数声明,先从本文件中寻找函数的定义,找不到,再从其他文件中寻找外部函数的定义。
变量的存储方式和生存期
变量的存在时间就是生存期,在程序运行的整个过程中都存在的变量是在内存的静态存储区存放的。有的变量则是在调用其所在函数时才临时分配存储单元,调用结束后该存储单元被释放,这种变量是存放在内存的动态存储区的。
每一个变量和函数都有数据类型和数据的存储类型
注意:建立存储空间的声明是定义声明(如int a;
),不需要建立存储空间的声明是引用声明(如:extern int a;
)
_undef,extern和函数声明注意的小知识点
#include <stdio.h>
#undef AB;
#undef AB;//我们可以多次终止一个宏名的作用域,及时它没有被定义
extern int a;
extern int a;//我们也可以多次扩展一个没有被定义的变量的作用域
void f(void);
void f(void);//我们也可以多次声明一个没有被定一个的函数
int main(void) {
return 0;
}
递归求5的阶乘
#include <stdio.h>
int f(int a) {
if (a == 1 || a == 0)
return 1;
else if (a > 1)
return a * f(a - 1);//递归直到a=1
else
return -1;//小于0的情况
}
int main(void) {
printf("%d", f(5));
return 0;
}
第八章 指针
指针数组和多重指针
指针数组:
1.一个数组,若起元素均为指针类型数据,成为指针数组。
2.定义指针数组的一般形式:类型名*数组名[数组长度];
3.如int *p[4];
,由于[]比*
优先级高,因此p先与[4]结合,形成p[4],这显然是数组的形式,表示p数组有4个元素。然后p[4]再和*
结合,表示此数组的每个元素都可以指向一个整型变量。
#include <stdio.h>
#include <string.h>
void show(char* a[4]) {
for (int i = 0; i < 4; i++) {
printf("%c\n", *a[i]);//调用指针数组中的指针
}
}
#include <stdio.h>
int main(void) {
char q = '1', w = '2', e = '3', r = '4';//定义四个字符
char* zz1 = &q;
char* zz2 = &w;
char* zz3 = &e;
char* zz4 = &r;//定义四个指针
char* a[4] = { zz1, zz2, zz3, zz4 };//将指针存入数组
show(a);
return 0;
}
多重指针:
1.利用指针访问一个变量就是“间接访问”
如果一个指证变量中存放了个目标变量的地址,这就是“单级间址”
如果一个指针变量中存放的是一个地址并且这个地址指向了一个目标变量,这就是“二级间址”;
间址方法可以衍生更多级,即多重指针。
指针数组做main函数形参
1.main函数无参时(如:int main(void)
)调用main函数时b不必给出实参。
2.main函数可以带参,例如int main(int argc , char * argv[])
argc
和argv
是main函数的形参,他们是程序的命令行参数.
argc表示参数个数,arcv表示参数向量,他是一个字符指针的指针变量,其每个元素指向一个字符串
3.一个带参main函数程序的运行
就是执行命令行里面的就行了。。。
void指针
1.void就是基类型的。
2.指向void类型可理解为“指向空类型”或者“不指向确定的类型”。也就是说指向的数据类型的字节长度不确定
void * p3;
//这个p3是不能直接用来访问的
//如果要用p3访问数据,需要强制转换成指定类型的指针才可以。
动态内存分配
1.全局变量在内存的静态存储区存放。
2.非静态的局部变量(包括形参)是分配在内存中的动态存储区的,这个区域称为栈,
3.c语言还允许建立内存动态分配区域,以存放一些临时用的数据,这些数据不必在程序的声明部分定义,也不必等到函数结束时才释放,需要时随时开辟,不需要时随时释放。这个存储区成为堆,此类数据只能通过指针来引用.
举个例子
输入学生数,再输入每个学生对应的姓名和成绩,最后把这些信息输出。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n;
printf("请输入学生数:");
scanf("%d", &n);
//void *malloc(unsigned int size)
//其功能是在堆区分配连续的size个字节的存储空间并且返回这段存储空间开头位置的地址
//如果内存分配失败,会返回空指针
int* p1 = (int*)malloc(sizeof(int) * n);
//上面这一句的意思就是一个int是4个字节,这里就是分配了连续的4乘以n个的内存空间
//并且把这个地址的首字节的地址赋值给了p1
if (p1 == NULL) {
printf("内存分配失败,程序退出\n");
exit(-1);//返回给操作系统-1,表示不是正常结束
}
char(*p2)[50] = (char (*)[50])calloc(n, sizeof(char) * 50);//指针数组
//calloc 函数被用来动态地分配内存以存储n个长度为50的字符数组。
if (p2 == NULL) {
printf("内存分配失败,程序退出\n");
exit(-1);//返回给操作系统-1,表示不是正常结束
}
for (int i = 0; i < n; ++i) {
printf("请输入第%d个学生的成绩和姓名:\n", i + 1);
scanf("%d", p1 + i);//通过指针接收
scanf("%s", p2[i]);//放到对应的指针数组中
}
for (int i = 0; i < n; ++i) {
printf("第%d个学生的成绩是%d,姓名是%s\n", i+1, *(p1 + i),p2[i]);
}
free(p1);//必须是一个指向动态内存空间的指针
free(p2);//释放内存
return 0;
}
后续的动态内存我就不写了,所以算了,暂时参考一下大纲,后续再继续深度学习c语言。
第九章 用户建立自己的数据类型
结构体定义和初始化
c语言允许用户自己建立由不同数据组成的组合型的数据结构,它称为结构体。在啊其他一些高级语言中称为“记录”。
定义结构体的一般形式
struc 结构体标记 //sruct+结构体标记,即结构体的类型名
#include <stdio.h>
#include <stdlib.h>
struct Student {
char name[30];
int age;
double score;
};//感觉就是类啊
int main(void) {
struct Student a;
printf("请输入学生的姓名,年龄和成绩:\n");
scanf("%s%d%lf", &a.name, &a.age, &a.score);
printf("学生的姓名:%s,年龄:%d,成绩:%lf", a.name, a.age, a.score);
return 0;
}
用指针也可以来访问这个类,如下
#include <stdio.h>
#include <stdlib.h>
struct Student {
char name[30];
int age;
double score;
};
int main(void) {
struct Student a;
printf("请输入学生的姓名,年龄和成绩:\n");
scanf("%s%d%lf", &a.name, &a.age, &a.score);
struct Student* p = &a;
printf("学生的姓名:%s,学生的年龄:%d,学生的成绩:%lf", p->name, p->age, p->score);//指针来访问
return 0;
}
下面解释一下,a.name
中间的.是结构体成员运算符,而p->name
指向结构体成员运算符。这两个运算符和圆括号方括号一样,都是优先级第一的结构运算符。结合方向都是自左往右的
上面的形式也可以用完全初始化和不完全初始化,如下
值得注意的是,结构体标记和变量至少有一种。如果有变量,可以进行初始化。
结构体类型的变量在内存中占的字节长度
字节对齐:各种数据类型是按照一定的规则在内存当中排列的。
基本数据类型变量的地址是能够被它们所占字节的长度所整除的。
关于结构体:
1.结构体变量的首地址能够被其最宽基本类型成员的大小所整除。
2.结构体成员相对于结构体变量的首地址的偏移量(结构体变量的首地址编号和结构体成员的地址编号他们之间所差的字节数)都是其成员大小的整数倍
3.结构体的总大小为结构最宽基本类型成员的整数倍
结构体数组和指针
输入n个学生的姓名和成绩并按成绩排序输出。
#include <stdio.h>
#define N 3
struct Stu {
char name[30];
double score;
};
void input(struct Stu* p, int len) {
for (int i = 0; i < len; ++i) {
printf("请输入第%d个学生的姓名和成绩:\n", i + 1);
scanf("%s%lf", &p[i].name, &p[i].score);//这里name是数组,其数组名本身就是地址,所以取地址符可以省略
//scanf("%s%lf", (p+i)->name, &(p+i)->score);//和上面一样的,只不过这个是通过指针来访问
}
}
void show(struct Stu* p, int len) {
for (int i = 0; i < len; ++i) {
printf("第%d个学生的姓名:%s,成绩:%lf\n", i + 1, p[i].name, p[i].score);
}
}
void sort(struct Stu* p, int len) {
// 冒泡排序
for (int i = 0; i < len - 1; i++) {
for (int j = 0; j < len - i - 1; j++) {
if (p[j].score > p[j + 1].score) {
// 交换位置
struct Stu temp = p[j];
p[j] = p[j + 1];
p[j + 1] = temp;
}
}
}
}
int main(void) {
struct Stu a[N];
input(a, N);
sort(a, N);
show(a, N);
return 0;
}
这里面就是有结构体数组了,然后就是用指针调用了数组里面的结构体。
如何理解复杂的数据类型
1.如何理解int (*p)[5]
的含义呢?
首先我们可以看看int[5],将int[5]当做一个数据类型,设其为A。则int a[5]
就相当于Aa.
同理我们可以把int (*p)[5]
转化成A *p
,因为A是一个含有5个整型元素数组类型,所以A*就代表是一个含有五个整型元素的数组的指针类型,那么p就是该类型的变量。
2.那么如何理解int (*p)(int)
所代表的含义呢?
我们把int (int)
当成一个数据类型,并设一个数据类型A和其等价,则int f(int)
和A f
等价。同理我们可以把int(*p)(int)
转换成A*p。
因为A是一个返回值是int,形参为int的函数类型,所以A*就是一个返回值是int,形参为int的函数的指针类型,那么p就是该类型的变量。
结论:看变量所属括号的最外层,把最外层的数据类型设为一个新的数据类型,然后去掉变量最外层的花括号。通过这种方法,知道原式转化成“数据类型 变量”的形式。
通过类似的方法,我们可以理解如下复杂类型的数据。
用typedef声明新类型名
按照定义变量的方式,把变量名换上新类型名,并且在最前面加typedef
,就声明了新类型名代表原来的类型。
typedef int count
这个时候count就代表的是int类型的数据。count a = 3;
这个a就是个整型数据。
typedef int Num[3]
定义了一个含有3个int类型的数组的数据类型。Num b={1,2,3}
其实就是别名
同样也可以定义结构体
#include <stdio.h>
typedef struct {
int a;
double b;
}Stu;
int main(void) {
Stu m = { 1,23 };
printf("%d\n%lf", m.a, m.b);
return 0;
}
链表
单向链表
一般头结点不存放有效数据(当然你也可以根据情况而定),在其他节点存放有效数据。
上一个节点的最后一个元素都是指向下一个节点的第一个元素的地址。
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node* pnext; // 指向下一个节点的指针
} N, * P;//这个P就是对Node的指针
P create(void) { // 创建链表头节点
P phead = (P)malloc(sizeof(N)); // 分配内存
if (phead == NULL) { // 检查内存是否分配成功
printf("分配内存失败,程序退出\n");
exit(-1);
}
phead->pnext = NULL; // 初始化链表头节点的指针域
return phead;
}
void input(P phead, int value) { // 添加节点到链表尾部
P last = phead;
while (last->pnext != NULL) // 找到链表最后一个节点
last = last->pnext;
P newnode = (P)malloc(sizeof(N)); // 分配新节点的内存
if (newnode == NULL) {
printf("分配内存失败,程序退出\n");
exit(-1);
}
newnode->data = value; // 设置新节点的数据
newnode->pnext = NULL; // 将新节点的指针域设为 NULL,表示新节点为链表的最后一个节点
last->pnext = newnode; // 将新节点连接到链表中,成为链表的最后一个节点
}
void show(P phead) { // 遍历并展示链表中的数据
P p = phead->pnext; // 获取链表的第一个节点
while (p != NULL) { // 循环遍历链表的每个节点
printf("%d\n", p->data); // 打印节点的数据
p = p->pnext; // 移动到下一个节点
}
}
int len(P phead) {
int sum = 0;
P p = phead->pnext; // 获取链表的第一个有效节点
while (p != NULL) { // 循环遍历链表的每个有效节点
sum++;
p = p->pnext; // 移动到下一个节点
}
return sum;
}
/*
初始化变量 sum 为 0。
将 p 指针指向链表的第一个有效节点,即指向节点 1。
进入循环。由于节点 1 不为 NULL,执行循环体。
sum 的值增加 1,现在 sum 的值为 1。
将 p 指针移动到下一个节点,即节点 2。
进入下一次循环。由于节点 2 不为 NULL,执行循环体。
sum 的值增加 1,现在 sum 的值为 2。
将 p 指针移动到下一个节点,即节点 3。
进入下一次循环。由于节点 3 不为 NULL,执行循环体。
sum 的值增加 1,现在 sum 的值为 3。
将 p 指针移动到下一个节点,即节点 4。
进入下一次循环。由于节点 4 不为 NULL,执行循环体。
sum 的值增加 1,现在 sum 的值为 4。
将 p 指针移动到下一个节点,即 NULL。
退出循环。由于 p 指针为 NULL,不满足循环条件,循环结束。
返回 sum 的值,即 4。
*/
bool del(P phead ,int index){//删除函数
if (index <= 0 || index > len(phead)) {
return false;
}
for (int i = 1; i < index; ++i) {
phead = phead->pnext;//这个时候的phead值就是我们要删除的节点的前一个节点的地址
}
P tmp = phead->pnext->pnext;//保存要删除的节点的下一个节点
free(phead->pnext);//释放要删除的节点
phead->pnext = tmp;//将删除的节点前后链接上
return true;
}
bool insert(P phead, int index, int value) {//插入函数
if (index <= 0 || index >= len(phead) + 1) {
return false;//判断index是否合法
}
P pnew = (P)malloc(sizeof(N));//给新的节点分配内存空间
if (pnew == NULL) {
printf("内存分配失败\n");
exit(-1);
}
pnew->data = value;
for (int i = 1; i < index; ++i) {
phead = phead->pnext;//这个时候的phead值就是我们要增加的节点的前一个节点的地址
}
P tmp = phead->pnext;//保存要增加的节点的下一个节点
phead->pnext = pnew;//把上一个节点的pnext指向新的pnew
pnew->pnext = tmp;//前一个节点的pnext值赋值给增加点节点的pnext
//完成
}
int main(void) {
P phead = create(); // 创建链表头节点
input(phead, 1); // 添加节点到链表中
input(phead, 2);
input(phead, 3);
input(phead, 4);
show(phead); // 展示链表中的数据
printf("此链表中含有的有效数据个数是:%d\n", len(phead));
if (del(phead, 3)) {
printf("删除成功\n");
}
else {
printf("删除失败\n");
}
show(phead);//删除后的
printf("此链表中含有的有效数据个数是:%d\n", len(phead));
if (insert(phead, 3, 5)) {//在3原来的地方插入一个5
printf("插入成功\n");
}
else {
printf("插入失败\n");
}
show(phead);//增加后的
printf("此链表中含有的有效数据个数是:%d\n", len(phead));
return 0;
}
理一下就清楚了,我已经加上了详细的注释,很简单滴~输出如下
共用体
共用体又称联合体,使用覆盖技术,几个变量相互覆盖,从而使几个不同变量共占同一段内存的结构。
#include <stdio.h>
union Data {//定义了一个共用体Data
int i;
char ch;
double f;//里面是成员表列
};
int main(void) {
union Data a;
//a = 3; 不可以直接用一个基本类型的数据对共用体变量赋值
a.i = 3;//这个就是可以的
a.ch = 'A';
printf("%d\n", a.i);
printf("%c\n", a.ch);
return 0;
}
值得注意的是,共用体的所有成员所使用的内存的开头地址的值都是一样的,所以当我们对其中一个成员赋值时,会把这个变量中原油数据诶给覆盖掉。举个例子
printf("%c\n%d\n", a.ch,a.i);//就会输出垃圾值
我这个编译器不会输出垃圾值啊,但是我看的视频的老师用的编译器会输出垃圾值。
共用体特征:
1.共用体变量的地址和长度必须被其最宽基本类型成员的大小所整除。
2.其总长度必须大于等于最宽成员的长度。
(注意一个是最宽基本类型一个是最宽)
枚举类型
1.指将变量的值一一列出来,变量的值只限于列举出来的值的范围内。
2.定义枚举类型的一般形式,enum 枚举名 {枚举元素表列} 枚举变量表列;
可以定义在函数内部或者函数外部,枚举名和枚举变量至少有一个即可。如果有变量,可以对变量进行初始化,定义枚举元素时也可显示的指定数值。
#include <stdio.h>
enum A {a1,a2,a3,a4};//c语言在定义的时候会根据这些元素的顺序定义一个默认值,分别为0.1.。。。。
enum B { b1=3, b2, b3, b4=5 };//定义枚举类型的时候可以为枚举元素指定一个值,为指定值的元素将是前一个元素+1
int main(void) {
enum A w = a2;
enum B w2 = b2;
printf("%d,%d,%d\n", a2, a3, b2);//输出124
return 0;
}
其中a2
是enum A
类型的常量,所以既然是常量,就不可能中途改变它的值。
下面是枚举类型的定义
第十章 文件的输入输出
文件的基本概念
一般把文件分为以下两个类别:
1.程序文件,这种文件的内容是程序代码。如源程序文件,目标文件,可执行文件等。
2.数据文件,其内容不是程序,而是提供程序运行时读写的数据。
注:所谓文件一般指存储在外部介质(如磁盘)上数据的集合。
终端即计算机的各种输入输出设备。操作系统把终端都统一作为文件来处理。例如:终端键盘是输入文件,显示屏和打印机是输出文件。
下面讲讲文件名
这是常识把,这个都不知道的立即推,退出安全圈。
数据文件
1.分类:ASCII文件(文本文件)和二进制文件(镜像文件)。存取一个二进制文件不需要转换,存取一个文本文件需要转换。如下,用16来举例子
2.数据从源到目的端的流动称为数据流
3.数据流按读写数据的类型值不同分为两种:字节流和字符流。两者的区别就是读写的时候一个是按字节读写,一个是按字符。
按照源和目的段的不同可分为:输入流(文件流向内存)和输出流(内存流向文件)和缓冲流。
文件缓冲区
C90采用缓冲文件系统处理数据文件,所谓缓冲文件系统是指系统自动地在内存区为程序中一些正在使用的文件开辟一个文件缓冲区。比如说:从内存向磁盘输出数据,可能会先送到内存的缓冲区中,装满缓冲区或者缓冲区被刷新后在一起送到磁盘去。
文件类型指针
1.每个被使用的文件都在内存中开辟一个响应的文件信息去,用来存放文件的有关信息。这些信息是保存在一个结构体变量中。给结构体是由系统声明,取名为FILE(在stdio.h中声明)
。
2.我们可以定义一个FILE类型的指针,然后使该指针指向某一个文件的文件信息区,那么我们就可以访问这个文件了。
文件的函数
fopen
1.打开是指为文件建立相应的信息区和文件缓冲区。
2.使用fopen打开文件,fopen的调用方式为:fopen(文件名,使用文件方式);
这个就和python里的fopen几乎一模一样啊喂!
如FILE *fp = fopen("a1","r");
fclose
1.关闭是指撤销文件信息区和文件缓冲区。
2.使用fclose(文件指针)
关闭数据文件。其作用就是把关闭文件指针所指向的文件关闭。
注:1.如果不关闭文件将会丢失数据。
因为我们向文件写数据时,缓冲区未充满而程序结束,就有可能使缓冲区中的数据丢失。而使用fclose函数关闭文件,会先把缓冲区中的数据输出到磁盘,然后才撤销文件信息区。
2.fclose成功执行之了关闭操作返回0,否则返回EOF(-1)
向文件读写字符
这里用一个例子解释,读入一个文本文件并把其内容输出到一个新的文本文件中
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char name[50] = "C:\\工具\\临时存放\\读.txt";//中间记得转义,当然直接用一个正斜杠也可以
FILE* fp = fopen(name, "r");//打开文件
if (fp == NULL) {
printf("文件打开失败,程序退出");
exit(-1);
}
FILE* fp2 = fopen("C:\\工具\\临时存放\\写.txt", "w");
if (fp2 == NULL) {
printf("文件打开失败,程序退出");
exit(-1);
}
char ch;
while ((ch = fgetc(fp)) != EOF) {//while中的条件是检测是否读取到文件末尾或者是否出错
//fgerc就是读取函数
fputc(ch, fp2);//将读取到的内容存到fp2中
}
fclose(fp);
fclose(fp2);
return 0;
}
成功创建名字为写的文件。
下面是值得注意点两点
用格式化的方式读写文件
1.1.printf和scanf函数是向终端进行格式话的输入和输出。如果向文件进行格式化输入输出需要用到fprintf和fscanf函数。调用方式如下:
fprintf(文件指针,格式字符串,输出列表);
fscaf(文件指针,格式字符串,输出列表);
#include <stdio.h>
#include <stdlib.h>
int main(void) {
FILE* p1 = fopen("C:\\工具\\临时存放\\读.txt","r");
FILE* p2 = fopen("C:\\工具\\临时存放\\写.txt", "w");
char c[40];
fscanf(p1, "%s", c);//读取文件中的字符存到c数组中去
fprintf (p2,"%s", c);//将c数组的东西输出到p2文件中去
return 0;
}
成功
用二进制的方式读写文件
用到fprintf
和fscanf
函数的时候,输入时要讲文件中的ASCII码转换成二进制形式保存在内存变量中,在输出时又要将内存中的二进制转换成字符,要花费较多的时间。
所以一般来说我们使用二进制的方式来读写文件
fread
和fwrite
函数
fread(buffer,size,count,fp)
:从fp所指向的文件中读取数据,最多读取count个项,每个项size个字节,并把这些数据存放到以buffer为地址开头的内存块中,如果调用成功返回实际读取到的的项个数,如果不成功或一开始读取到文件末尾返回0。
:把以buffer为地址开头的内存块的count个项的size个字节的数据写入fp中所指向的文件中。返回实际写入的数据项个数count。
#include <stdio.h>
#include <stdlib.h>
typedef struct {
char name[30];
double score;
}Stu;
int main(void) {
Stu a[3] = { {"zhangsan",89.5},{"lisi",78},{"wangwu",90.1}};//初始化了一个结构体数组
FILE* p1 = fopen("C:\\工具\\临时存放\\写.txt", "wb");
fwrite(a, sizeof(Stu), 3, p1);//fwrite(buffer,size,count,fp)
fclose(p1);
p1 = NULL;//防止出现野指针
FILE* p2 = fopen("C:\\工具\\临时存放\\写.txt", "rb");//读取文件
Stu b[3];
fread(b, sizeof(Stu), 3, p2);//从p2指向的文件中读入数据,放到b中
for (int i = 0; i < 3; ++i) {
printf("第%d个,姓名:%s,分数:%lf\n", i + 1, b[i].name, b[i].score);//输出b中的内容
}
fclose(p2);
p2 = NULL;
return 0;
}
文件读写的注意事项
1.在windows中,文本已”\r\n”代表换行。也就是是两个字符表示换行。
你fputs写入换行符\n的时候,函数会自动在\n面前加上\r。
二进制的换行符就是\n
随机读写数据
1.随机读写数据不是按照数据在文件中的物理次序进行读写。而是可以对任何位置上的数据进行访问。显然这种方式比顺序访问效率高得多。
2.文件位置标记简称文件标记,用来指示“接下来要读写的下一个字符的位置”
3.rewind(fp);
:使fp所指向的文件的文件标记到文件的开头位置,此函数没有返回值。
4.fseek(文件类型指针,位移量,起始点;)
可改变文件标记,执行成功返回0,否则返回非0值。
下面是一个例子
#include <stdio.h>
int main(void) {
FILE* fp = fopen("C:\\工具\\临时存放\\读.txt", "r");
if (fp == NULL) {
printf("文件打开失败\n");
return -1;
}
// 示例:使用 rewind(fp) 将文件指针设置为开头位置
rewind(fp);
// 示例:使用 fseek(fp, offset, origin) 将文件指针定位到相应位置
fseek(fp, 10, SEEK_SET); // 将文件指针定位到距离开头位置 10 个字节的位置
// 其他操作...
fclose(fp);
return 0;
}
注意,使用fseek()
函数定位文件指针时要确保文件是以对应的模式(如”r”或”w”)打开的,否则可能会引发错误。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。后续可能会有评论区,不过也可以在github联系我。