C语言学习记录

本文最后更新于:2023年11月10日 上午

C 数据类型

C 中的类型可分为以下几种:

image-20231110092838613

整数类型

image-20231110093736344

注意,各种类型的存储大小与系统位数有关,但目前通用的以64位系统为主。

浮点类型

image-20231110093854912

void类型

void 类型指定没有可用的值。它通常用于以下三种情况下:

image-20231110093933055

C 变量

基本类型:

image-20231110094029970

1.注意,赋值表达式有返回值,等于等号右边的值

1
2
3
4
int x, y;
x = 1;
y = (x = 2 * x);
//变量 y 的值就是赋值表达式( x = 2 * x )的返回值 2

2.头文件 stdbool.h 定义了另一个类型别名 bool ,并且定义了 true 代表 1 、 false 代表 0 。只要加载 这个头文件,就可以使用这几个关键字。

1
2
3
4
5
#include<stdbool.h>

bool flag = false;

//加载头文件 stdbool.h 以后,就可以使用 bool 定义布尔值类型,以及 false 和 true 表示真伪。

C 数组

数组的地址

数组是一连串连续储存的同类型值,只要获得起始地址(首个成员的内存地址),就能推算出其他成员 的地址。请看下面的例子。

1
2
3
4
5
6
7
int a[5] = {11, 22, 33, 44, 55};
int* p;
p = &a[0];
printf("%d\n", *p); //11

//&a[0] 就是数组 a 的首个成员 11 的内存地址,也是整个数组的起始地址。反过来,从这个地址( *p ),可以获得首个成员的值 11

由于数组的起始地址是常用操作, &array[0] 的写法有点麻烦,C 语言提供了便利写法,数组名等同于 起始地址,也就是说,数组名就是指向第一个成员( array[0] )的指针。

1
2
3
4
int a[5] = {11, 22, 33, 44, 55};
int* p = &a[0];
// 等同于
int* p = a;

上面示例中, &a[0] 和数组名 a 是等价的。

这样的话,如果把数组名传入一个函数,就等同于传入一个指针变量。在函数内部,就可以通过这个指 针变量获得整个数组。

函数接受数组作为参数,函数原型可以写成下面这样。

1
2
3
4
5
// 写法一
int sum(int arr[], int len);
// 写法二
int sum(int* arr, int len);

上面示例中,传入一个整数数组,与传入一个整数指针是同一回事,数组符号 [] 与指针符号 * 是可以互 换的。下一个例子是通过数组指针对成员求和

1
2
3
4
5
6
7
8
9
10
int sum(int* arr, int len) {
int i;
int total = 0;
// 假定数组有 10 个成员
for (i = 0; i < len; i++) {
total += arr[i];
}
return total;
}
//示例中,传入函数的是一个指针 arr (也是数组名)和数组长度,通过指针获取数组的每个成员,从而求和。

*& 运算符也可以用于多维数组。

1
2
3
4
5
int a[4][2];
// 取出 a[0][0] 的值
*(a[0]);
// 等同于
**a

上面示例中,由于 a[0] 本身是一个指针,指向第二维数组的第一个成员 a[0][0] 。所以, *(a[0]) 取 出的是 a[0][0] 的值。至于 **a ,就是对 a 进行两次 * 运算,第一次取出的是 a[0] ,第二次取出的是 a[0][0] 。同理,二维数组的 &a[0][0] 等同于 *a

不能将一个数组名赋值给另外一个数组名。

数组指针的加减法

1
2
3
4
int a[5] = {11, 22, 33, 44, 55};
for (int i = 0; i < 5; i++) {
printf("%d\n", *(a + i));
}

上面示例中,通过指针的移动遍历数组, a + i 的每轮循环每次都会指向下一个成员的地址, *(a + i) 取出该地址的值,等同于 a[i] 。对于数组的第一个成员, *(a + 0) (即 *a )等同于 a[0]

由于数组名与指针是等价的,所以下面的等式总是成立。

1
a[b] == *(a + b)

上面代码给出了数组成员的两种访问方式,一种是使用方括号 a[b] ,另一种是使用指针 *(a + b)

如果指针变量 p 指向数组的一个成员,那么 p++ 就相当于指向下一个成员,这种方法常用来遍历数组。

1
2
3
4
5
6
7
8
int a[] = {11, 22, 33, 44, 55, 999};
int* p = a;
while (*p != 999) {
printf("%d\n", *p);
p++;
}
//示例中,通过 p++ 让变量 p 指向下一个成员。
//注意,数组名指向的地址是不能变的,所以例中,不能直接对 a 进行自增,即 a++ 的写法是错的,必须将 a 的地址赋值给指针变量 p ,然后对 p 进行自增

同一个数组的两个成员的指针相减时,返回它们之间的距离

1
2
3
4
int* p = &a[5];
int* q = &a[1];
printf("%d\n", p - q); // 4
printf("%d\n", q - p); // -4

C 流程控制

break 命令只能跳出循环体和 switch 结构,不能跳出 if 结构。

1
2
3
4
5
if (n > 1) {
if (n > 2) break; // 无效
printf("hello\n");
}
//示例中, break 语句是无效的,因为它不能跳出外层的 if 结构。

函数

main()

C 语言规定, main() 是程序的入口函数,即所有的程序一定要包含一个 main() 函数。程序总是从这个 函数开始执行,如果没有该函数,程序就无法启动。其他函数都是通过它引入程序的。

函数指针

对于任意函数,都有五种调用函数的写法。

1
2
3
4
5
6
7
8
9
10
11
12
// 写法一
print(10)
// 写法二
(*print)(10)
// 写法三
(&print)(10)
// 写法四
(*print_ptr)(10)
// 写法五
print_ptr(10)

//为了简洁易读,一般情况下,函数名前面都不加 * 和 & 。

struct 结构

C 语言内置的数据类型,除了最基本的几种原始类型,只有数组属于复合类型,可以同时包含多个值, 但是只能包含相同类型的数据,实际使用中并不够用。

实际使用中,主要有下面两种情况,需要更灵活强大的复合类型。

  • 复杂的物体需要使用多个变量描述,这些变量都是相关的,最好有某种机制将它们联系起来。
  • 某些函数需要传入多个参数,如果一个个按照顺序传入,非常麻烦,最好能组合成一个复合结构传 入。

为了解决这些问题,C 语言提供了 struct 关键字,允许自定义复合数据类型,将不同类型的值组合在一 起。这样不仅为编程提供方便,也有利于增强代码的可读性。C 语言没有其他语言的对象(object)和 类(class)的概念,struct 结构很大程度上提供了对象和类的功能。

下面是 struct 自定义数据类型的一个例子。

1
2
3
4
struct fraction {
int numerator;
int denominator;
};

上面示例定义了一个分数的数据类型 struct fraction ,包含两个属性 numerator 和 denominator 。

注意,作为一个自定义的数据类型,它的类型名要包括 struct 关键字,比如上例是 struct fraction ,单独的 fraction 没有任何意义,另外, struct 语句结尾的分号不能省略

定义了新的数据类型以后,就可以声明该类型的变量,这与声明其他类型变量的写法是一样的。

1
2
3
4
5
struct fraction f1;
f1.numerator = 22;
f1.denominator = 7;

//这里先声明了一个 struct fraction 类型的变量 f1 ,这时编译器就会为 f1 分配内存,接着就可以为 f1 的不同属性赋值。struct 结构的属性通过点( . )来表示,比如 numerator 属性要写成 f1.numerator 。
1
2
3
4
5
6
7
8
9
10
//除了逐一对属性赋值,也可以使用大括号,一次性对 struct 结构的所有属性赋值
struct car {
char* name;
float price;
int speed;
};
struct car saturn = {"Saturn SL/2", 16000.99, 175}
//大括号里面的值的顺序,必须与 struct 类型声明时属性的顺序一致。否则,必须为每个值指定属性名。
struct car saturn = {.speed=172, .name="Saturn SL/2"};
saturn.speed = 168;

struct 的数据类型声明语句与变量的声明语句,可以合并为一个语句。

1
2
3
4
5
6
typedef struct cell_phone {
int cell_no;
float minutes_of_charge;
} phone;
phone p = {5551234, 5}

指针变量也可以指向 struct 结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct book {
char title[500];
char author[100];
float value;
}* b1;
// 或者写成两个语句
struct book {
char title[500];
char author[100];
float value;
};
struct book* b1;
//变量 b1 是一个指针,指向的数据是 struct book 类型的实例。

struct 指针

如果将 struct 变量传入函数,函数内部得到的是一个原始值的副本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
struct turtle {
char* name;
char* species;
int age;
};
void happy(struct turtle t) {
t.age = t.age + 1;
}
int main() {
struct turtle myTurtle = {"MyTurtle", "sea turtle", 99};
happy(myTurtle);
printf("Age is %i\n", myTurtle.age); // 输出 99
return 0;
}

上面示例中,函数 happy() 传入的是一个 struct 变量 myTurtle ,函数内部有一个自增操作。但是,执行完 happy() 以后,函数外部的 age 属性值根本没变。原因就是函数内部得到的是 struct 变量的副本, 改变副本影响不到函数外部的原始数据

通常,希望传入函数的是同一份数据,函数内部修改数据以后,会反映在函数外部。而且,传入的是同一份数据,也有利于提高程序性能。这时就需要将 struct 变量的指针传入函数,通过指针来修改 struct 属性,就可以影响到函数外部。

struct 指针传入函数的写法如下:

1
2
3
4
5
void happy(struct turtle* t) {
}
happy(&myTurtle);
//这里 t 是 struct 结构的指针,调用函数时传入的是指针
//struct 类型跟数组不一样,类型标识符本身并不是指针,所以传入时,指针必须写成 &myTurtle 。

函数内部也必须使用 (*t).age 的写法,从指针拿到 struct 结构本身:

1
2
3
void happy(struct turtle* t) {
(*t).age = (*t).age + 1;
}

上面示例中, (*t).age 不能写成 *t.age ,因为点运算符 . 的优先级高于 **t.age 这种写法会将 t.age 看成一个指针,然后取它对应的值,会出现无法预料的结果。

现在,重新编译执行上面的整个示例, happy() 内部对 struct 结构的操作,就会反映到函数外部。

(*t).age 这样的写法很麻烦。C 语言就引入了一个新的箭头运算符->,可以从 struct 指针上直接 获取属性,大大增强了代码的可读性。

1
2
3
void happy(struct turtle* t) {
t->age = t->age + 1;
}

总结一下,对于 struct 变量名,使用点运算符. 获取属性;对于 struct 变量指针,使用箭头运算符 -> 获取属性。

以变量 myStruct 为例,假设 ptr 是它的指针,那么下面三种写法是同一回事。

1
2
// ptr == &myStruct
myStruct.prop == (*ptr).prop == ptr->prop

指针

简介

指针是什么?首先,它是一个值,这个值代表一个内存地址,因此指针相当于指向某个内存地址的路标。

字符 * 表示指针,通常跟在类型关键字的后面,表示指针指向的是什么类型的值。比如, char* 表示一 个指向字符的指针, float* 表示一个指向 float 类型的值的指针。

1
2
int* intPtr;
//上面示例声明了一个变量 intPtr ,它是一个指针,指向的内存地址存放的是一个整数。

星号 * 可以放在变量名与类型关键字之间的任何地方,下面的写法都是有效的

1
2
3
int   *intPtr;
int * intPtr;
int* intPtr;

& 运算符与 * 运算符互为逆运算,下面的表达式总是成立。

1
2
int i = 5;
if (i == *(&i)) // 正确

指针变量的初始化

声明指针变量之后,编译器会为指针变量本身分配一个内存空间,但是这个内存空间里面的值是随机 的,也就是说,指针变量指向的值是随机的。这时一定不能去读写指针变量指向的地址,因为那个地址 是随机地址,很可能会导致严重后果。

1
2
int* p;
*p = 1; // 错误

上面的代码是错的,因为 p 指向的那个地址是随机的,向这个随机地址里面写入 1 ,会导致意想不到的 结果。 正确做法是指针变量声明后,必须先让它指向一个分配好的地址,然后再进行读写,这叫做指针变量的 初始化。

1
2
3
4
int* p;
int i;
p = &i;
*p = 13;

上面示例中, p 是指针变量,声明这个变量后, p 会指向一个随机的内存地址。这时要将它指向一个已 经分配好的内存地址,上例就是再声明一个整数变量 i ,编译器会为 i 分配内存地址,然后让 p 指向 i 的内存地址( p = &i; )。完成初始化之后,就可以对 p 指向的内存地址进行赋值了( *p = 13; )。

typedef 命令

typedef 命令用来为某个类型起别名。

1
2
typedef type name;
//上面代码中, type 代表类型名,name 代表别名。

比如下面的:

1
2
3
4
typedef int INTEGER;
INTEGER a, b;
a = 1;
b = 2;

INTEGER a, b;等效于int a, b;

typedef 可以一次指定多个别名

1
typedef int antelope, bagel, mushroom;

typedef 可以为指针起别名。

1
2
3
4
typedef int* intptr;
int a = 10;
intptr x = &a;
//上面示例中, intptr 是 int* 的别名。不过,使用的时候要小心,这样不容易看出来,变量 x 是一个指针类型。

typedef结构体类型定义别名:

1
2
3
4
5
typedef struct stu{
char name[20];
int age;
char sex;
} STU;

STU 是 struct stu 的别名,可以用 STU 定义结构体变量:

1
STU body1,body2;

它等价于:

1
struct stu body1, body2;

typedef 也可以用来为数组类型起别名。

1
2
typedef int five_ints[5];
five_ints x = {11, 22, 33, 44, 55};

typedef 为函数起别名的写法如下。

1
2
typedef signed char (*fp)(void);
//上面示例中,类型别名 fp 是一个指针,代表函数 signed char (*)(void) 。

主要好处

(1)更好的代码可读性。

1
2
3
typedef char* STRING;
STRING name;
//上面示例为字符指针起别名为 STRING ,以后使用 STRING 声明变量时,就可以轻易辨别该变量是字符串。

(2)为 struct、union、enum 等命令定义的复杂数据结构创建别名,从而便于引用。

1
2
3
4
5
struct treenode {
// ...
};
typedef struct treenode* Tree;
//上面示例中, Tree 为 struct treenode* 的别名。

typedef 也可以与 struct 定义数据类型的命令写在一起

1
2
3
4
5
typedef struct animal {
char* name;
int leg_count, speed;
} animal;
//上面示例中,自定义数据类型时,同时使用 typedef 命令,为 struct animal 起了一个别名 animal 。

这种情况下,C 语言允许省略 struct 命令后面的类型名。

1
2
3
4
5
typedef struct {
char *name;
int leg_count, speed;
} animal;
//上面示例相当于为一个匿名的数据类型起了别名 animal 。

(3)typedef 方便以后为变量改类型。

1
2
typedef float app_float;
app_float f1, f2, f3;

上面示例中,变量 f1 、 f2 、 f3 的类型都是 float 。如果以后需要为它们改类型,只需要修改 typedef 语句即可。

1
typedef long double app_float;

上面命令将变量 f1 、 f2 、 f3 的类型都改为 long double

typedef 和 #define 的区别

typedef 在表现上有时候类似于 #define,但它和宏替换之间存在一个关键性的区别。正确思考这个问题的方法就是把 typedef 看成一种彻底的“封装”类型,声明之后不能再往里面增加别的东西。

  1. 可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做。如下所示:
1
2
3
4
5
#define INTERGE int
unsigned INTERGE n; //没问题

typedef int INTERGE;
unsigned INTERGE n; //错误,不能在 INTERGE 前面添加 unsigned
  1. 在连续定义几个变量的时候,typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证。例如:
1
2
#define PTR_INT int *
PTR_INT p1, p2;

经过宏替换以后,第二行变为:

1
int *p1, p2;

这使得 p1、p2 成为不同的类型:p1 是指向 int 类型的指针,p2 是 int 类型。

相反,在下面的代码中:

1
2
typedef int * PTR_INT
PTR_INT p1, p2;

p1、p2 类型相同,它们都是指向 int 类型的指针。

其他

如果在int型变量的声明中为变量赋应该实数值的初始值(如3.14或5.7等)会怎么样?

  • 会直接舍去小数部分,只保留整数部分

单目运算符&(取值运算符)

  • &a,取得a的地址(生产指向a的指针)

单目运算符*(指针运算符)

  • *a,a指向的对象

赋值表达式的左操作数不可以是数组名。

p指向x时,*px的别名

指针p指向数组中的元素e

  • p + i为指向元素e后第i个元素的指针
  • p - i为指向元素e前第i个元素的指针
  • 指向元素e后第i个元素的*(p + i),可以写为p[i]
  • 指向元素e前第i个元素的*(p - i),可以写为p[-i]
image-20230917142303857
image-20230917142327597

c语言编译阶段出现如下的问题:

c00e0930273a652d2e9d0d0b218eb41

可以检查一下是不是有正在运行的端口没关闭!!!


C语言学习记录
http://viper2383.github.io/2023/09/17/C语言学习/
作者
w1per3
发布于
2023年9月17日
许可协议