c语言学习-2

1. 指针

1. 介绍

指针是一个变量,其值为另一个变量的地址,即,指针变量的值就是地址。指针变量声明的一般形式为:

1
type *var-name;

其中,type 是指针的基类型,var-name 是指针变量的名称。* 表示这是一个指针变量。

例如,声明一个指向整数类型的指针变量:int *ip;

指针变量的值是一个地址,可以使用 & 运算符获取变量的地址,并将其赋值给指针变量。例如:

1
2
3
int var = 10;
int *ip;
ip = &var;

在上述代码中,ip 指向变量 var 的地址。可以使用 * 运算符获取指针变量所指向的值。例如:

1
2
3
4
5
int var = 10;
int *ip;
ip = &var;
printf("Value of var variable: %d\n", var);
printf("Value available at the address stored in ip variable: %d\n", *ip);

输出结果为:

1
2
Value of var variable: 10
Value available at the address stored in ip variable: 10

指针变量可以指向数组、函数、结构体等数据类型。指针变量也可以指向指针,即指向另一个指针变量的地址。

2. 指针运算

指针运算包括指针的算术运算和关系运算。

指针的算术运算包括指针加法、指针减法和指针比较。指针加法是指将指针变量与一个整数相加,得到一个新的指针,指向原指针所指向的地址加上整数所表示的偏移量。例如:

1
2
3
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("%d\n", *(p + 1)); // 输出 2

指针减法是指将两个指针相减,得到两个指针之间的元素个数。例如:

1
2
3
4
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = arr;
int *p2 = arr + 2;
printf("%d\n", p2 - p1); // 输出 2

指针比较是指比较两个指针的大小,即比较它们所指向的地址的大小。例如:

1
2
3
4
5
6
7
8
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = arr;
int *p2 = arr + 2;
if (p1 < p2) {
printf("p1 is less than p2\n");
} else {
printf("p1 is greater than or equal to p2\n");
}

输出结果为:

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
2
char b=a+12;
printf("%d,%d\n", 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码表。

ASCII码表

仅供参考,不需记住,了解就行

接下来,你猜猜,如果通过地址修改值会怎么样呢?

1
2
3
4
int a=5;
int *p=&a;
*p=6;
printf("%d\n", a);

输出结果为:

1
6

这就是指针的作用,通过地址修改值。

之前讲函数的时候,我们说函数的参数传递是值传递,但是有时候我们需要修改参数的值,现在有了指针,是不是就可以修改了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}

int main() {
int x = 5;
int y = 10;
printf("Before swap: x = %d, y = %d\n", x, y);
swap(&x, &y);
printf("After swap: x = %d, y = %d\n", x, y);
return 0;
}

结果是:

1
2
Before swap: x = 5, y = 10
After swap: x = 10, y = 5

这就说明,指针可以修改参数的值。

在C语言中,你是没试过传数组进函数?如果试过,是不是在函数内部操作数组也可以改变数组中的值?

你可以想想为什么吗?

  1. 指针与数组

指针与数组的关系非常密切,因为数组在内存中是连续存储的,所以可以通过指针来访问数组中的元素。例如:

1
2
3
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("%d\n", *(p + 1)); // 输出 2

这里,p指向数组的第一个元素,p + 1指向数组的第二个元素,*(p + 1)就是数组的第二个元素。

也可以这样:

1
2
3
4
5
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i));
}

输出结果为:

1
1 2 3 4 5

或者

1
2
3
4
int arr[5] = {1, 2, 3, 4, 5};
for (int i=0;i<5;i++,arr++) {
printf("%d ", *arr);
}

输出结果为:

1
1 2 3 4 5

是不是很神奇,这说明了什么,数组其实就是指针,指针就是数组,数组就是指针。

但是广义上,指针不等同于数组,数组是连续存储的,而指针可以指向任意位置。

你可以把数组看作是指针,所以在c语言中可以直接作为参数,直接通过指针访问。

不过,当你看到可以吧数组看做指针的是候,有没有想过数组的一些妙用呢?数组是不是只能用来装数字呢?

一个简单的例子:二维数组

1
2
3
4
int (*arr1)[3] = (int (*)[3])malloc(sizeof(int) * 3 * 3);
int *arr2[3] = (int **)malloc(sizeof(int *) * 3);
int arr3[3][3] = {0};
int ** arr4 = (int **)malloc(sizeof(int *) * 3);

这四种定义方式有什么区别呢?

第一种:arr1是一个指向3个int类型的指针,也就是一个3行1列的二维数组。

第二种:arr2是一个指向3个int类型的指针的指针,也就是一个3行3列的二维数组。

第三种:arr3是一个3行3列的二维数组。

第四种:arr4是一个指向3个int类型的指针的指针,也就是一个3行3列的二维数组。

看似作用都是一样的,但是实际上,在有些地方,他们就是不同。

例:

1
2
3
4
5
6
7
8
int cmp(const void *a, const void *b) {
return (*(int **)a)[0] - (*(int **)b)[0];
}

int main() {
int arr[][2] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
qsort(arr, 9, sizeof(int*), cmp);//你觉得会错吗?
}

考虑到可能不知道上述代码是啥意思,我来对内置函数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
2
3
int cmp(const void *a, const void *b) {
return (*(int **)a)[0] - (*(int **)b)[0];//先强转成int **,然后取第一维
}

但是,原数组定义的arr是int [][2]指针,这里一定会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Line 4:
AddressSanitizer:DEADLYSIGNAL
=================================================================
==23==ERROR: AddressSanitizer: SEGV on unknown address 0x00009fff8000 (pc 0x5567addc9eca bp 0x7fffb81a1890 sp 0x7fffb81a1890 T0)
==23==The signal is caused by a READ memory access.
#0 0x5567addc9eca in cmp solution.c:4
#1 0x7f6da35220a2 in qsort_r ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:10019
#2 0x5567addca307 in largestValsFromLabels solution.c:4
#3 0x5567addc9739 in main solution.c:4
#4 0x7f6da2bca1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 6d64b17fbac799e68da7ebd9985ddf9b5cb375e6)
#5 0x7f6da2bca28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 6d64b17fbac799e68da7ebd9985ddf9b5cb375e6)
#6 0x5567addc9db4 in _start (solution+0x1fdb4) (BuildId: 843b218276aa8fd89da0af30c1e791c0d3b84953)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV solution.c:4 in cmp
==23==ABORTING

这个例子告诉我们,指针的使用一定要小心,要不然错了都不知道为什么,同时,我们也进一步了解数组的牛逼之处。

关于指针的其他用法,在后文请以自己的思考,自行探索。

另外,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
2
3
4
5
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
printf("Failed to open file.\n");
return 1;
}

这里拿到的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
2
3
4
5
6
int num;
size_t result = fread(&num, sizeof(int), 1, fp);
if (result != 1) {
printf("Failed to read file.\n");
return 1;
}

要从文件中写入一个整数,可以使用以下代码:

1
2
3
4
5
6
int num = 123;
size_t result = fwrite(&num, sizeof(int), 1, fp);
if (result != 1) {
printf("Failed to write file.\n");
return 1;
}

2.3 文件关闭

在C语言中,使用fclose函数关闭文件,该函数的原型如下:

1
int fclose(FILE *stream);

其中,stream是指向FILE对象的指针,表示要关闭的文件。

例如,要关闭一个名为"test.txt"的文件,可以使用以下代码:

1
2
3
4
5
int result = fclose(fp);
if (result != 0) {
printf("Failed to close file.\n");
return 1;
}

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
2
3
4
5
6
struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
...
数据类型 成员n;
};

例如,定义一个学生结构体,包含姓名、年龄和成绩三个成员:

1
2
3
4
5
struct Student {
char name[20];
int age;
float score;
}

然后这么定义一个学生:

1
struct Student stu1 = {"欣冻", 18, 96};

调用

1
printf("%s %d %.2f\n", stu1.name, stu1.age, stu1.score);

3.2 结构体指针

结构体指针是指向结构体的指针变量。结构体指针可以用来访问结构体的成员,也可以用来动态分配内存。

例如,定义一个学生结构体,并创建一个结构体指针:

1
2
3
4
5
6
7
struct Student {
char name[20];
int age;
float score;
};

struct Student *pstu = (struct Student *)malloc(sizeof(struct Student));

然后这么定义一个学生:

1
2
3
strcpy(pstu->name, "欣冻");
pstu->age = 18;
pstu->score = 96;

调用

1
printf("%s %d %.2f\n", pstu->name, pstu->age, pstu->score);

3.3 结构体数组

结构体数组是指包含多个结构体元素的数组。结构体数组可以用来存储多个结构体数据。

例如,定义一个学生结构体,并创建一个结构体数组:

1
2
3
4
5
6
7
struct Student {
char name[20];
int age;
float score;
};

struct Student stu[10];

然后这么定义一个学生:

1
2
3
strcpy(stu[0].name, "欣冻");
stu[0].age = 18;
stu[0].score = 96;

调用

1
printf("%s %d %.2f\n", stu[0].name, stu[0].age, stu[0].score);

这里我就不扯太远,这些都是最基本的运用。

3.4 typedef

typedef是C语言中的一个关键字,用于为数据类型定义别名。typedef可以用于为基本数据类型、结构体、联合体、枚举等定义别名。

例如,定义一个学生结构体,并使用typedef为它定义一个别名:

1
2
3
4
5
6
7
struct Student {
char name[20];
int age;
float score;
};

typedef struct Student Stu;

也可以

1
2
3
4
5
typedef struct {
char name[20];
int age;
float score;
} Stu;

然后这么定义一个学生:

1
Stu stu1 = {"欣冻", 18, 96};

调用

1
printf("%s %d %.2f\n", stu1.name, stu1.age, stu1.score);

4. 总结

好了,c语言的基础就到这里了,当然,还有很多内容,比如指针、函数、内存管理、数据结构、算法等等,这些都需要你自己去学习,这里只是给你一个入门,让你知道c语言是什么,怎么用,怎么学。

希望对你有所帮助。