c语言学习-2
c语言学习-2
1. 指针
1. 介绍
指针是一个变量,其值为另一个变量的地址,即,指针变量的值就是地址。指针变量声明的一般形式为:
1 | type *var-name; |
其中,type 是指针的基类型,var-name 是指针变量的名称。* 表示这是一个指针变量。
例如,声明一个指向整数类型的指针变量:int *ip;
指针变量的值是一个地址,可以使用 & 运算符获取变量的地址,并将其赋值给指针变量。例如:
1 | int var = 10; |
在上述代码中,ip 指向变量 var 的地址。可以使用 * 运算符获取指针变量所指向的值。例如:
1 | int var = 10; |
输出结果为:
1 | Value of var variable: 10 |
指针变量可以指向数组、函数、结构体等数据类型。指针变量也可以指向指针,即指向另一个指针变量的地址。
2. 指针运算
指针运算包括指针的算术运算和关系运算。
指针的算术运算包括指针加法、指针减法和指针比较。指针加法是指将指针变量与一个整数相加,得到一个新的指针,指向原指针所指向的地址加上整数所表示的偏移量。例如:
1 | int arr[5] = {1, 2, 3, 4, 5}; |
指针减法是指将两个指针相减,得到两个指针之间的元素个数。例如:
1 | int arr[5] = {1, 2, 3, 4, 5}; |
指针比较是指比较两个指针的大小,即比较它们所指向的地址的大小。例如:
1 | int arr[5] = {1, 2, 3, 4, 5}; |
输出结果为:
1 | p1 is less than p2 |
当然,实际上这个比较没有什么卵用
3. 指针与储存原理
这里,就不得不感叹计算机这个伟大发明了,我们以前都学过二进制,以前在学二进制的时候是否感觉二进制没什么卵用,想必是吧!
但是计算机可不如我们人类的大脑,它只能识别二进制,但也是二进制铸就了现代世界。
在之前将变量的时候,我们说int类型占4个字节,32位,啥意思呢?
就比如a=5,在计算机中存储的时候,就是00000000 00000000 00000000 00000101,这32位二进制数,就是5,这就是储存原理,计算机储存数据就是二进制,而二进制就是32位,所以int类型占4个字节,32位。前面的0可不是写着玩的,而是实实在在存在计算机里的,那计算机又怎么知道这个a就是5呢?这就需要地址了,计算机知道这个数在内存中的地址,然后通过地址找到这个数,这就是地址的作用。你如果打印&a,就会打印出这个数的地址,也就是这个数在内存中的位置,这就是地址。
1 | printf("%p\n", &a); |
输出结果为:
1 | 0x7ffeedd2e4d4 |
这个地址就是a在内存中的位置,也就是a的地址。
那既然是这样,作为变量储存就有上限了,四个字节的有符号(注意,32位的首位是符号位,只有无符号整数才不包括符号位。),int就只能储存-2147483648到2147483647的数。
请看下标
| 类型 | 储存位数 |
|---|---|
| char | 8位 |
| int | 32位 |
| float | 32位 |
| double | 64位 |
这里我定义一个char类型的变量
1 | char a=126; |
如果我这样操作
1 | char b=a+12; |
你觉得会如何输出呢
我猜会不会是 138,138
但是实际上输出的是
1 | -118,138 |
这是为什么呢?
因为char类型只有8位,也就是1个字节,也就是8位二进制,也就是2^8=256,所以char类型的变量最大只能储存-128到127的数,但是a=126,所以a+12=138,但是138超出了char类型的范围,所以b=138-256=-118。
那如果我想输出138呢?
这就需要强制类型转换了,将char类型强制转换为int类型,这样就可以输出138了。
1 | int b=a+12; |
这样就会向高位转换。
你或许会有疑问,为什么char可以存数字,有这个疑问说明你没有完全理解二进制存储。
你认为一个字母是怎么存入计算机的呢?
计算机只认识二进制,所以字母也要转换成二进制,其转换关系,就是大名鼎鼎的ASCII码表。
仅供参考,不需记住,了解就行
接下来,你猜猜,如果通过地址修改值会怎么样呢?
1 | int a=5; |
输出结果为:
1 | 6 |
这就是指针的作用,通过地址修改值。
之前讲函数的时候,我们说函数的参数传递是值传递,但是有时候我们需要修改参数的值,现在有了指针,是不是就可以修改了。
1 | void swap(int *a, int *b) { |
结果是:
1 | Before swap: x = 5, y = 10 |
这就说明,指针可以修改参数的值。
在C语言中,你是没试过传数组进函数?如果试过,是不是在函数内部操作数组也可以改变数组中的值?
你可以想想为什么吗?
- 指针与数组
指针与数组的关系非常密切,因为数组在内存中是连续存储的,所以可以通过指针来访问数组中的元素。例如:
1 | int arr[5] = {1, 2, 3, 4, 5}; |
这里,p指向数组的第一个元素,p + 1指向数组的第二个元素,*(p + 1)就是数组的第二个元素。
也可以这样:
1 | int arr[5] = {1, 2, 3, 4, 5}; |
输出结果为:
1 | 1 2 3 4 5 |
或者
1 | int arr[5] = {1, 2, 3, 4, 5}; |
输出结果为:
1 | 1 2 3 4 5 |
是不是很神奇,这说明了什么,数组其实就是指针,指针就是数组,数组就是指针。
但是广义上,指针不等同于数组,数组是连续存储的,而指针可以指向任意位置。
你可以把数组看作是指针,所以在c语言中可以直接作为参数,直接通过指针访问。
不过,当你看到可以吧数组看做指针的是候,有没有想过数组的一些妙用呢?数组是不是只能用来装数字呢?
一个简单的例子:二维数组
1 | int (*arr1)[3] = (int (*)[3])malloc(sizeof(int) * 3 * 3); |
这四种定义方式有什么区别呢?
第一种:arr1是一个指向3个int类型的指针,也就是一个3行1列的二维数组。
第二种:arr2是一个指向3个int类型的指针的指针,也就是一个3行3列的二维数组。
第三种:arr3是一个3行3列的二维数组。
第四种:arr4是一个指向3个int类型的指针的指针,也就是一个3行3列的二维数组。
看似作用都是一样的,但是实际上,在有些地方,他们就是不同。
例:
1 | int cmp(const void *a, const void *b) { |
考虑到可能不知道上述代码是啥意思,我来对内置函数qsort做出解释。
顾名思义,qsort就是快速排序(quicksort),快速排序是一种常用的排序算法,其时间复杂度为O(nlogn),简单实现见排序算法。
c语言的内置qsort源码定义
1 | void __cdecl qsort(void *_Base,size_t _NumOfElements,size_t _SizeOfElements,int (__cdecl *_PtFuncCompare)(const void *,const void *)); |
是不是有点看不懂,没关系,我也看不懂,但是我们只需要了解怎么用。
第一个参数:待排序的数组的首地址
第二个参数:待排序的数组中元素个数
第三个参数:待排序的数组中每个元素的大小
第四个参数:比较函数,用于比较两个元素的大小,如果第一个参数大于第二个参数,返回正数,如果第一个参数等于第二个参数,返回0,如果第一个参数小于第二个参数,返回负数。
所以,我们只需要定义一个比较函数,然后调用qsort就可以了。
之前的写法就是按照二维数组的第一维排序
1 | int cmp(const void *a, const void *b) { |
但是,原数组定义的arr是int [][2]指针,这里一定会报错。
1 | Line 4: |
这个例子告诉我们,指针的使用一定要小心,要不然错了都不知道为什么,同时,我们也进一步了解数组的牛逼之处。
关于指针的其他用法,在后文请以自己的思考,自行探索。
另外,qsort在算法里面真的很有用,请自行了解使用,不会问ai。
2. 文件操作
文件操作是C语言中非常重要的一部分,它可以让程序读写文件,实现数据的持久化存储。文件操作主要包括文件的打开、关闭、读写、定位等操作。
2.1 文件打开
在C语言中,使用fopen函数打开文件,该函数的原型如下:
1 | FILE *fopen(const char *filename, const char *mode); |
其中,filename是要打开的文件名,mode是打开文件的模式,可以是以下几种之一:
- “r”:只读模式,打开一个已存在的文件,如果文件不存在,则返回NULL。
- “w”:只写模式,打开一个文件用于写入,如果文件不存在,则创建一个新文件。
- “a”:追加模式,打开一个文件用于追加,如果文件不存在,则创建一个新文件。
- “r+”:读写模式,打开一个文件用于读写,如果文件不存在,则返回NULL。
- “w+”:读写模式,打开一个文件用于读写,如果文件不存在,则创建一个新文件。
- “a+”:读写模式,打开一个文件用于读写,如果文件不存在,则创建一个新文件。
- “b”:二进制模式,表示以二进制方式打开文件,可以与其他模式组合使用,例如"rb"、“wb”、“ab”、“r+b”、“w+b”、"a+b"等。
例如,要打开一个名为"test.txt"的文件,并以只读模式打开,可以使用以下代码:
1 | FILE *fp = fopen("test.txt", "r"); |
这里拿到的fp是一个FILE类型的指针,它指向打开的文件,后续的文件操作都需要通过这个指针进行。(也就是这个文件的指针)
2.2 文件读写
在C语言中,使用fread和fwrite函数进行文件的读写操作,该函数的原型如下:
1 | size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); |
其中,ptr是指向数据缓冲区的指针,size是每个数据项的大小,nmemb是要读取或写入的数据项的数量,stream是指向FILE对象的指针,表示要读取或写入的文件。
例如,要从文件中读取一个整数,可以使用以下代码:
1 | int num; |
要从文件中写入一个整数,可以使用以下代码:
1 | int num = 123; |
2.3 文件关闭
在C语言中,使用fclose函数关闭文件,该函数的原型如下:
1 | int fclose(FILE *stream); |
其中,stream是指向FILE对象的指针,表示要关闭的文件。
例如,要关闭一个名为"test.txt"的文件,可以使用以下代码:
1 | int result = fclose(fp); |
2.4 文件与输入输出
在C语言中,你知道scanf,printf,它们是标准输入输出。那么,原理呢?
其实,scanf和printf都是基于文件操作的。它们分别对应于标准输入文件stdin和标准输出文件stdout。
标准输入文件stdin是一个预定义的文件指针,它指向标准输入设备,通常是键盘。标准输出文件stdout是一个预定义的文件指针,它指向标准输出设备,通常是屏幕。
一切的一切,都是路径,一切的一切,都是文件。
要是你不想从stdin读取数据,也可以用fscanf,从文件读取数据,只要把stdin换成文件指针(即FILE *fp)即可。
同理,fprintf也可以输出到文件,只要把stdout换成文件指针即可。
2.5 文件的其他内置函数
除了上述的函数,文件操作还有其他一些常用的内置函数,例如:
- fseek函数:用于定位文件指针的位置,该函数的原型如下:
1 | int fseek(FILE *stream, long offset, int whence); |
其中,stream是指向FILE对象的指针,表示要定位的文件;offset是偏移量,表示要移动的字节数;whence是起始位置,可以是以下几种之一:
-
SEEK_SET:从文件开头开始计算偏移量。
-
SEEK_CUR:从当前位置开始计算偏移量。
-
SEEK_END:从文件末尾开始计算偏移量。
-
ftell函数:用于获取文件指针的当前位置,该函数的原型如下:
1 | long ftell(FILE *stream); |
其中,stream是指向FILE对象的指针,表示要获取位置的文件。
- rewind函数:用于将文件指针重新定位到文件开头,该函数的原型如下:
1 | void rewind(FILE *stream); |
其中,stream是指向FILE对象的指针,表示要重新定位的文件。
- feof函数:用于判断文件指针是否已经到达文件末尾,该函数的原型如下:
1 | int feof(FILE *stream); |
关于如何使用,自己可以尝试一下。(我基本没用过文件操作)
到了这里,你就快把c的所有基础学完了(至少我了解的),你知道了内部储存的原理,输出输入的方法(想跟深入了解就是去学习计算机组成原理吧!)
3. 结构体
好了,来到最后一个内容,结构体是一种用户自定义的数据类型,它可以包含多个不同类型的数据成员。结构体是一种复合数据类型,它可以将多个不同类型的数据组合在一起,形成一个整体。
3.1 定义结构体
1 | struct 结构体名 { |
例如,定义一个学生结构体,包含姓名、年龄和成绩三个成员:
1 | struct Student { |
然后这么定义一个学生:
1 | struct Student stu1 = {"欣冻", 18, 96}; |
调用
1 | printf("%s %d %.2f\n", stu1.name, stu1.age, stu1.score); |
3.2 结构体指针
结构体指针是指向结构体的指针变量。结构体指针可以用来访问结构体的成员,也可以用来动态分配内存。
例如,定义一个学生结构体,并创建一个结构体指针:
1 | struct Student { |
然后这么定义一个学生:
1 | strcpy(pstu->name, "欣冻"); |
调用
1 | printf("%s %d %.2f\n", pstu->name, pstu->age, pstu->score); |
3.3 结构体数组
结构体数组是指包含多个结构体元素的数组。结构体数组可以用来存储多个结构体数据。
例如,定义一个学生结构体,并创建一个结构体数组:
1 | struct Student { |
然后这么定义一个学生:
1 | strcpy(stu[0].name, "欣冻"); |
调用
1 | printf("%s %d %.2f\n", stu[0].name, stu[0].age, stu[0].score); |
这里我就不扯太远,这些都是最基本的运用。
3.4 typedef
typedef是C语言中的一个关键字,用于为数据类型定义别名。typedef可以用于为基本数据类型、结构体、联合体、枚举等定义别名。
例如,定义一个学生结构体,并使用typedef为它定义一个别名:
1 | struct Student { |
也可以
1 | typedef struct { |
然后这么定义一个学生:
1 | Stu stu1 = {"欣冻", 18, 96}; |
调用
1 | printf("%s %d %.2f\n", stu1.name, stu1.age, stu1.score); |
4. 总结
好了,c语言的基础就到这里了,当然,还有很多内容,比如指针、函数、内存管理、数据结构、算法等等,这些都需要你自己去学习,这里只是给你一个入门,让你知道c语言是什么,怎么用,怎么学。
希望对你有所帮助。





