C语言入门教程25-typedef类型别名
最后更新于:2022-04-01 20:27:03
本文目录
- [一、typedef作用简介](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#label0)
- [二、typedef与指针](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#label1)
- [三、typedef与结构体](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#label2)
- [三、typedef与指向结构体的指针](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#label3)
- [四、typedef与枚举类型](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#label4)
- [五、typedef与指向函数的指针](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#label5)
- [六、typedef与#define](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#label6)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
这讲介绍C语言中很常用的一个关键字---typedef。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#labelTop)
### 一、typedef作用简介
* 我们可以使用typedef关键字为各种数据类型定义一个新名字(别名)。
~~~
1 #include
2
3 typedef int Integer;
4 typedef unsigned int UInterger;
5
6 typedef float Float;
7
8 int main(int argc, const char * argv[]) {
9 Integer i = -10;
10 UInterger ui = 11;
11
12 Float f = 12.39f;
13
14 printf("%d %d %.2f", i, ui, f);
15
16 return 0;
17 }
~~~
在第3、第4、第6行分别给int、unsigned int、float起了个别名,然后在main函数中使用别名定义变量,用来跟原来的基本类型是完全一样的。输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767cabf43.png)
当然,给类型起别名后,原来的int、float还是可以正常使用的:
~~~
int i = 10;
float f = 10.0f;
~~~
* 也可以在别名的基础上再起一个别名
~~~
typedef int Integer;
typedef Integer MyInteger;
~~~
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#labelTop)
### 二、typedef与指针
除开可以给基本数据类型起别名,typedef也可以给指针起别名
~~~
1 #include
2
3 typedef char *String;
4
5 int main(int argc, const char * argv[]) {
6 // 相当于char *str = "This is a string!";
7 String str = "This is a string!";
8
9 printf("%s", str);
10
11 return 0;
12 }
~~~
在第3给指针类型char *起别名为String,然后在第7行使用String定义了一个字符串,是不是有点Java的感觉?
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#labelTop)
### 三、typedef与[结构体](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html)
给结构体起别名可以使代码更加简洁明
### 1.默认情况下结构体变量的使用
~~~
1 // 定义一个结构体
2 struct MyPoint {
3 float x;
4 float y;
5 };
6
7 int main(int argc, const char * argv[]) {
8 // 定义结构体变量
9 struct MyPoint p;
10 p.x = 10.0f;
11 p.y = 20.0f;
12
13 return 0;
14 }
~~~
默认情况下,我们定义结构体变量需要带个struct关键字,看第9行
### 2.使用typedef给结构体起别名
~~~
1 // 定义一个结构体
2 struct MyPoint {
3 float x;
4 float y;
5 };
6
7 // 起别名
8 typedef struct MyPoint Point;
9
10 int main(int argc, const char * argv[]) {
11 // 定义结构体变量
12 Point p;
13 p.x = 10.0f;
14 p.y = 20.0f;
15
16 return 0;
17 }
~~~
我们在第8行给结构体MyPoint起了个别名叫做Point,然后在12行使用Point定义了一个结构体变量p,不用再带上struct关键字了
其实第1~第8行的代码可以简写为:
~~~
// 定义一个结构体,顺便起别名
typedef struct MyPoint {
float x;
float y;
} Point;
~~~
甚至可以省略结构体名称:
~~~
typedef struct {
float x;
float y;
} Point;
~~~
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#labelTop)
### 三、typedef与指向[结构体](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html)的指针
typedef可以给指针、结构体起别名,当然也可以给指向结构体的指针起别名
~~~
1 #include
2
3 // 定义一个结构体并起别名
4 typedef struct {
5 float x;
6 float y;
7 } Point;
8
9 // 起别名
10 typedef Point *PP;
11
12 int main(int argc, const char * argv[]) {
13 // 定义结构体变量
14 Point point = {10, 20};
15
16 // 定义指针变量
17 PP p = &point;
18
19 // 利用指针变量访问结构体成员
20 printf("x=%f,y=%f", p->x, p->y);
21 return 0;
22 }
~~~
在第4行定义了一个结构体,顺便起了个别名叫Point,第10行为指向结构体的指针定义了别名PP。然后在main函数中使用这2个别名。
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767cbaf91.png)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#labelTop)
### 四、typedef与[枚举类型](http://www.cnblogs.com/mjios/archive/2013/03/24/2979390.html)
使用typedef给枚举类型起别名也可以使代码简洁。
~~~
1 // 定义枚举类型
2 enum Season {spring, summer, autumn, winter};
3 // 给枚举类型起别名
4 typedef enum Season Season;
5
6 int main(int argc, const char * argv[]) {
7 // 定义枚举变量
8 Season s = spring;
9
10 return 0;
11 }
~~~
在第2行定义了枚举类型,在第4行起了别名为Season,然后在第8行直接使用别名定义枚举变量,不用再带上enum关键字了。
第1行~第4行代码可以简化为:
~~~
// 定义枚举类型,并且起别名
typedef enum Season {spring, summer, autumn, winter} Season
~~~
甚至可以省略枚举名称,简化为:
~~~
typedef enum {spring, summer, autumn, winter} Season;
~~~
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#labelTop)
### 五、typedef与[指向函数的指针](http://www.cnblogs.com/mjios/archive/2013/03/19/2967037.html)
1.先来回顾下函数指针的知识
~~~
1 #include
2
3 // 定义一个sum函数,计算a跟b的和
4 int sum(int a, int b) {
5 int c = a + b;
6 printf("%d + %d = %d", a, b, c);
7 return c;
8 }
9
10 int main(int argc, const char * argv[]) {
11 // 定义一个指向sum函数的指针变量p
12 int (*p)(int, int) = sum;
13
14 // 利用指针变量p调用sum函数
15 (*p)(4, 5);
16
17 return 0;
18 }
~~~
* 在第4行定义了一个sum函数,第12行定义了一个指向sum函数的指针变量p,可以发现,这个指针变量p的定义比一般的指针变量看来复杂多了,不利于理解。
* 第15行调用了p指向的sum函数,输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767cc9ff9.png)
2.为了简化代码和方便理解,我们可以使用typedef给指向函数的指针类型起别名
~~~
1 #include
2
3 // 定义一个sum函数,计算a跟b的和
4 int sum(int a, int b) {
5 int c = a + b;
6 printf("%d + %d = %d", a, b, c);
7 return c;
8 }
9
10 typedef int (*MySum)(int, int);
11
12 int main(int argc, const char * argv[]) {
13 // 定义一个指向sum函数的指针变量p
14 MySum p = sum;
15
16 // 利用指针变量p调用sum函数
17 (*p)(4, 5);
18
19 return 0;
20 }
~~~
* 看第10行,意思是:给指向函数的指针类型,起了个别名叫MySum,被指向的函数接收2个int类型的参数,返回值为int类型。
* 在第14行直接用别名MySum定义一个指向sum函数的指针变量p,这样看起来简单舒服多了。第17行的函数调用是一样的。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2979482.html#labelTop)
### 六、typedef与#define
1.先来看看下面的两段代码有什么区别(注意每一段的第1行代码)
* 第1段
~~~
1 typedef char *String;
2
3 int main(int argc, const char * argv[]) {
4 String str = "This is a string!";
5 return 0;
6 }
~~~
* 第2段
~~~
1 #define String char *
2
3 int main(int argc, const char * argv[]) {
4 String str = "This is a string!";
5 return 0;
6 }
~~~
上面的两段代码只是第1行代码不一样,运行的效果都是一样的:定义了一个字符串"This is a string!"。
但它们的实现方式是不一样的:
- 第1段代码是用typedef给char *定义别名为String
- 第2段代码是用char *代替代码中的宏名String
只看上面两段代码,似乎看不太出typedef和#define的区别。
2.再来看一段代码
~~~
1 typedef char *String1;
2
3 #define String2 char *
4
5 int main(int argc, const char * argv[]) {
6 String1 str1, str2;
7
8 String2 str3, str4;
9 return 0;
10 }
~~~
第1行给char *起了个别名String1,第2行定义了宏String2。然后在第6、第8行定义了4个变量。
重点来了,注意:在这种情况下,只有str1、str2、str3才是指向char类型的指针变量,str4只是个char类型的变量。
下面简单分析一下原因:
* 如果连续声明两个int类型的变量,我们可以这样写:
~~~
int a, b;
~~~
上面的代码相当于:
~~~
int a;
int b;
~~~
* 以此类推
~~~
1 typedef char *String1;
2
3 String1 str1, str2;
~~~
经过typedef处理后,String1也算是一种数据类型,所以第3行代码相当于
~~~
1 String1 str1;
2 String1 str2;
~~~
由于String1就是char *,所以上面的两行代码等于
~~~
char *str1;
char *str2;
~~~
* 再看看宏定义的情况
~~~
1 #define String2 char *
2
3 String2 str3, str4;
~~~
因为宏定义纯粹是字符串替换,用char *代替String2,所以第3行代码相当于
~~~
char * str3, str4;
~~~
其实也就相当于:
~~~
char * str3;
char str4;
~~~
可以看出,只有str4是基本数据类型,str1、str2、str3都是指针类型。
所以,以后给类型起别名,最好使用typedef,而不是使用#define
';
C语言入门教程24-结构体
最后更新于:2022-04-01 20:27:00
本文目录
- [一、什么是结构体](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#label0)
- [二、结构体的定义](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#label1)
- [三、结构体变量的定义](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#label2)
- [四、结构体的注意点](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#label3)
- [五、结构体的初始化](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#label4)
- [六、结构体的使用](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#label5)
- [七、结构体数组](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#label6)
- [八、结构体作为函数参数](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#label7)
- [九、指向结构体的指针](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#label8)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
C语言的核心部分都说得七七八八了,相信大家已经对C语言的基本数据类型(char\int\float)、数组、指针都很熟悉了,今天来学习C语言中另外一种数据类型:结构体。在iOS开发中,结构体是经常用到的数据类型,使用频率不亚于指针,所以需要重视,不过用法非常简单。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#labelTop)
### 一、什么是结构体
* 在[第八讲](http://www.cnblogs.com/mjios/archive/2013/03/15/2961147.html)的时候已经介绍了C语言中的数组,用法跟其他语言差不多。当一个整体由多个数据构成时,我们可以用数组来表示这个整体,但是数组有个特点:内部的每一个元素都必须是相同类型的数据。
* 在实际应用中,我们通常需要由不同类型的数据来构成一个整体,比如学生这个整体可以由姓名、年龄、身高等数据构成,这些数据都具有不同的类型,姓名可以是字符串类型,年龄可以是整型,身高可以是浮点型。
* 为此,C语言专门提供了一种构造类型来解决上述问题,这就是[结构体](http://baike.baidu.com/view/204974.htm),它允许内部的元素是不同类型的。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#labelTop)
### 二、结构体的定义
### 1.定义形式
结构体内部的元素,也就是组成成分,我们一般称为"成员"。
结构体的一般定义形式为:
~~~
1 struct 结构体名{
2
3 类型名1 成员名1;
4
5 类型名2 成员名2;
6
7 ……
8
9 类型名n 成员名n;
10
11 };
~~~
struct是关键字,是结构体类型的标志。
### 2.举例
比如,我们定义一个学生
~~~
struct Student {
char *name; // 姓名
int age; // 年龄
float height; // 身高
};
~~~
上面定义了一个叫做Student的结构体,共有name、age、height3个成员。呵呵,看到这里是否有点面向对象的味道呢,其实这跟面向对象完全是两码事,只能说感觉有点像。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#labelTop)
### 三、结构体变量的定义
前面只是定义了名字为Student的结构体类型,并非定义了一个结构体变量,就像int一样,只是一种类型。
接下来定义一个结构体变量,方式有好多种。
### 1.先定义结构体类型,再定义变量
~~~
1 struct Student {
2 char *name;
3 int age;
4 };
5
6 struct Student stu;
~~~
第6行定义了一个结构体变量,变量名为stu。struct和Student是连着使用的。
### 2.定义结构体类型的同时定义变量
~~~
struct Student {
char *name;
int age;
} stu;
~~~
结构体变量名为stu
### 3.直接定义结构体类型变量,省略类型名
~~~
struct {
char *name;
int age;
} stu;
~~~
结构体变量名为stu
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#labelTop)
### 四、结构体的注意点
### 1.不允许对结构体本身递归定义
如下做法是错误的,注意第3行
~~~
1 struct Student {
2 int age;
3 struct Student stu;
4 };
~~~
### 2.结构体内可以包含别的结构体
~~~
1 struct Date {
2 int year;
3 int month;
4 int day;
5 };
6
7 struct Student {
8 char *name;
9 struct Date birthday;
10 };
~~~
注意第9行
### 3.定义结构体类型,只是说明了该类型的组成情况,并没有给它分配存储空间,就像系统不为int类型本身分配空间一样。只有当定义属于结构体类型的变量时,系统才会分配存储空间给该变量
~~~
1 struct Student {
2 char *name;
3 int age;
4 };
5
6 struct Student stu;
~~~
第1~4行并没有分配存储空间,当执行到第6行时,系统才会分配存储空间给stu变量。
### 4.结构体变量占用的内存空间是其成员所占内存之和,而且各成员在内存中按定义的顺序依次排列
比如下面的Student结构体:
~~~
1 struct Student {
2 char *name; // 姓名
3 int age; // 年龄
4 float height; // 身高
5 };
~~~
在16位编译器环境下,一个Student变量共占用内存:2 + 2 + 4 = 8字节。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#labelTop)
### 五、结构体的初始化
将各成员的初值,按顺序地放在一对大括号{}中,并用逗号分隔,一一对应赋值。
比如初始化Student结构体变量stu
~~~
1 struct Student {
2 char *name;
3 int age;
4 };
5
6 struct Student stu = {"MJ", 27};
~~~
只能在定义变量的同时进行初始化赋值,初始化赋值和变量的定义不能分开,下面的做法是错误的:
~~~
struct Student stu;
stu = {"MJ", 27};
~~~
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#labelTop)
### 六、结构体的使用
### 1.一般对结构体变量的操作是以成员为单位进行的,引用的一般形式为:结构体变量名.成员名
~~~
1 struct Student {
2 char *name;
3 int age;
4 };
5
6 struct Student stu;
7
8 // 访问stu的age成员
9 stu.age = 27;
~~~
第9行对结构体的age成员进行了赋值。"."称为成员运算符,它在所有运算符中优先级最高
### 2.如果某个成员也是结构体变量,可以连续使用成员运算符"."访问最低一级成员
~~~
1 struct Date {
2 int year;
3 int month;
4 int day;
5 };
6
7 struct Student {
8 char *name;
9 struct Date birthday;
10 };
11
12 struct Student stu;
13
14 stu.birthday.year = 1986;
15 stu.birthday.month = 9;
16 stu.birthday.day = 10;
~~~
注意第14行以后的代码
### 3.相同类型的结构体变量之间可以进行整体赋值
~~~
1 struct Student {
2 char *name;
3 int age;
4 };
5
6 struct Student stu1 = {"MJ", 27};
7
8 // 将stu1直接赋值给stu2
9 struct Student stu2 = stu1;
10
11 printf("age is %d", stu2.age);
~~~
注意第9行。输出结果为:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767bb5446.png)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#labelTop)
### 七、结构体数组
### 1.定义
跟结构体变量一样,结构体数组也有3种定义方式
~~~
struct Student {
char *name;
int age;
};
struct Student stu[5]; //定义1
~~~
~~~
struct Student {
char *name;
int age;
} stu[5]; //定义2
~~~
~~~
struct {
char *name;
int age;
} stu[5]; //定义3
~~~
上面3种方式,都是定义了一个变量名为stu的结构体数组,数组元素个数是5
### 2.初始化
~~~
struct {
char *name;
int age;
} stu[2] = { {"MJ", 27}, {"JJ", 30} };
~~~
也可以用数组下标访问每一个结构体元素,跟普通数组的用法是一样的
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#labelTop)
### 八、结构体作为函数参数
将结构体变量作为函数参数进行传递时,其实传递的是全部成员的值,也就是将实参中成员的值一一赋值给对应的形参成员。因此,形参的改变不会影响到实参。
~~~
1 #include
2
3 // 定义一个结构体
4 struct Student {
5 int age;
6 };
7
8 void test(struct Student stu) {
9 printf("修改前的形参:%d \n", stu.age);
10 // 修改实参中的age
11 stu.age = 10;
12
13 printf("修改后的形参:%d \n", stu.age);
14 }
15
16 int main(int argc, const char * argv[]) {
17
18 struct Student stu = {30};
19 printf("修改前的实参:%d \n", stu.age);
20
21 // 调用test函数
22 test(stu);
23
24
25 printf("修改后的实参:%d \n", stu.age);
26 return 0;
27 }
~~~
* 首先在第4行定义了一个结构体类型Student
* 在第18行定义了一个结构体变量stu,并在第22行将其作为实参传入到test函数
输出结果为:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767bc7139.png)
,形参是改变了,但是实参一直没有变过
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html#labelTop)
### 九、指向结构体的指针
* 每个结构体变量都有自己的存储空间和地址,因此指针也可以指向结构体变量
* 结构体指针变量的定义形式:struct 结构体名称 *指针变量名
* 有了指向结构体的指针,那么就有3种访问结构体成员的方式
- 结构体变量名.成员名
- (*指针变量名).成员名
- 指针变量名->成员名
~~~
1 #include
2
3 int main(int argc, const char * argv[]) {
4 // 定义一个结构体类型
5 struct Student {
6 char *name;
7 int age;
8 };
9
10 // 定义一个结构体变量
11 struct Student stu = {"MJ", 27};
12
13 // 定义一个指向结构体的指针变量
14 struct Student *p;
15
16 // 指向结构体变量stu
17 p = &stu;
18
19 /*
20 这时候可以用3种方式访问结构体的成员
21 */
22 // 方式1:结构体变量名.成员名
23 printf("name=%s, age = %d \n", stu.name, stu.age);
24
25 // 方式2:(*指针变量名).成员名
26 printf("name=%s, age = %d \n", (*p).name, (*p).age);
27
28 // 方式3:指针变量名->成员名
29 printf("name=%s, age = %d \n", p->name, p->age);
30
31 return 0;
32 }
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767bd83de.png)
';
C语言入门教程23-枚举
最后更新于:2022-04-01 20:26:58
本文目录
- [一、枚举的概念](http://www.cnblogs.com/mjios/archive/2013/03/24/2979390.html#label0)
- [二、枚举类型的定义](http://www.cnblogs.com/mjios/archive/2013/03/24/2979390.html#label1)
- [三、枚举变量的定义](http://www.cnblogs.com/mjios/archive/2013/03/24/2979390.html#label2)
- [四、枚举使用的注意](http://www.cnblogs.com/mjios/archive/2013/03/24/2979390.html#label3)
- [五、枚举变量的基本操作](http://www.cnblogs.com/mjios/archive/2013/03/24/2979390.html#label4)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
上一讲介绍了[结构体](http://www.cnblogs.com/mjios/archive/2013/03/24/2977910.html)类型,这讲就介绍C语言中的另一种数据类型---枚举类型。枚举类型在iOS中也是很常用的,用法跟Java中的枚举类似。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2979390.html#labelTop)
### 一、枚举的概念
枚举是C语言中的一种基本数据类型,并不是构造类型,它可以用于声明一组常数。当一个变量有几个固定的可能取值时,可以将这个变量定义为枚举类型。比如,你可以用一个枚举类型的变量来表示季节,因为季节只有4种可能的取值:春天、夏天、秋天、冬天。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2979390.html#labelTop)
### 二、枚举类型的定义
一般形式为:enum 枚举名 {枚举元素1,枚举元素2,……};
~~~
enum Season {spring, summer, autumn, winter};
~~~
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2979390.html#labelTop)
### 三、枚举变量的定义
前面只是定义了枚举类型,接下来就可以利用定义好的枚举类型定义变量。
跟结构体一样,有3种方式定义枚举变量
### 1.先定义枚举类型,再定义枚举变量
~~~
enum Season {spring, summer, autumn, winter};
enum Season s;
~~~
### 2.定义枚举类型的同时定义枚举变量
~~~
enum Season {spring, summer, autumn, winter} s;
~~~
### 3.省略枚举名称,直接定义枚举变量
~~~
enum {spring, summer, autumn, winter} s;
~~~
上面三种方式定义的都是枚举变量s
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2979390.html#labelTop)
### 四、枚举使用的注意
1> C语言编译器会将枚举元素(spring、summer等)作为整型常量处理,称为枚举常量。
2> 枚举元素的值取决于定义时各枚举元素排列的先后顺序。默认情况下,第一个枚举元素的值为0,第二个为1,依次顺序加1。
~~~
enum Season {spring, summer, autumn, winter};
~~~
也就是说spring的值为0,summer的值为1,autumn的值为2,winter的值为3
3> 也可以在定义枚举类型时改变枚举元素的值
~~~
enum season {spring, summer=3, autumn, winter};
~~~
没有指定值的枚举元素,其值为前一元素加1。也就说spring的值为0,summer的值为3,autumn的值为4,winter的值为5
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/24/2979390.html#labelTop)
### 五、枚举变量的基本操作
### 1.赋值
可以给枚举变量赋枚举常量或者整型值
~~~
enum Season {spring, summer, autumn, winter} s;
s = spring; // 等价于 s = 0;
s = 3; // 等价于 s = winter;
~~~
### 2.遍历枚举元素
~~~
enum Season {spring, summer, autumn, winter} s;
// 遍历枚举元素
for (s = spring; s <= winter; s++) {
printf("枚举元素:%d \n", s);
}
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767aba335.png)
';
C语言入门教程22-变量类型与作用域
最后更新于:2022-04-01 20:26:56
本文目录
- [ 一、变量的作用域](http://www.cnblogs.com/mjios/archive/2013/03/21/2973719.html#label0)
- [二、变量的存储类型](http://www.cnblogs.com/mjios/archive/2013/03/21/2973719.html#label1)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
C语言有丰富的数据类型和运算符,因此计算能力非常强大,计算过程中使用的值一般用变量来存储。变量也是有分类型的,不同类型的变量有不同的存储类型、不同的生命周期、不同的作用域,C语言也提供了一些关键字来设置变量的属性(比如设置存储类型、生命周期)。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/21/2973719.html#labelTop)
### 一、变量的作用域
C语言根据变量作用域的不同,将变量分为局部变量和全局变量。
### 1.局部变量
1> 定义:在函数内部定义的变量,称为局部变量。形式参数也属于局部变量。
2> 作用域:局部变量只在定义它的函数内部有效,即局部变量只有在定义它的函数内部使用,其它函数不能使用它。
### 2.全局变量
1> 定义:在所有函数外部定义的变量,称为全局变量。
2> 作用域:全局变量的作用范围是从定义变量的位置开始到源程序结束,即全局变量可以被在其定义位置之后的其它函数所共享。
~~~
1 int a;
2
3 int main ()
4 {
5 int b;
6 return 0;
7 }
~~~
第1行的变量a是全局变量,第5行的变量b是局部变量。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/21/2973719.html#labelTop)
### 二、变量的存储类型
* 变量的存储类型就是指变量存储在什么地方。有3个地方可以用于存储变量:普通内存、运行时堆栈、硬件寄存器。变量的存储类型决定了变量何时创建、何时销毁以及它的值能保持多久,也就是决定了变量的生命周期。
* C语言根据变量的存储类型的不同,可以把变量分为:自动变量、静态变量、寄存器变量。
### 1.自动变量
1> 定义:自动变量是存储在堆栈中的。
2> 哪些是自动变量:被关键字auto修饰的局部变量都是自动变量,但是极少使用这个关键字,基本上是废的,因为所有的局部变量在默认情况下都是自动变量。
3> 生命周期:在程序执行到声明自动变量的代码块(函数)时,自动变量才被创建;当自动变量所在的代码块(函数)执行完毕后,这些自动变量就会自行销毁。如果一个函数被重复调用,这些自动变量每次都会重新创建。
~~~
1 void test(int a, int b) {
2 int c = a + b;
3
4 auto int d;
5 }
~~~
第1行的变量a、b,第2行的变量c、第4行的变量d都是自动变量。
### 2.静态变量
1> 定义:静态变量是存储在静态内存中的,也就是不属于堆栈。
2> 哪些是静态变量:
- 所有的全局变量都是静态变量
- 被关键字static修饰的局部变量也是静态变量
3> 生命周期:静态变量在程序运行之前创建,在程序的整个运行期间始终存在,直到程序结束。
~~~
1 #include
2
3 int a;
4
5 void test() {
6 static int b = 0;
7 b++;
8
9 int c = 0;
10 c++;
11
12 printf("b=%d, c=%d \n", b, c);
13 }
14
15 int main() {
16 int i;
17 // 连续调用3次test函数
18 for (i = 0; i<3; i++) {
19 test();
20 }
21
22 return 0;
23 }
~~~
* 第3行的变量a、第6行的变量b都是静态变量,第9行的变量c、第16行的变量i是自动变量。
* 因为第6行的变量b是静态变量,所以它只会被创建一次,而且生命周期会延续到程序结束。因为它只会创建一次,所以第6行代码只会执行一次,下次再调用test函数时,变量b的值不会被重新初始化为0。
* 注意:虽然第6行的变量b是静态变量,但是只改变了它的存储类型(即生命周期),并没有改变它的作用域,变量b还是只能在test函数内部使用。
* 我们在main函数中重复调用test函数3次,输出结果为:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767a5cda5.png)
### 3.寄存器变量
1> 定义:存储在硬件寄存器中的变量,称为寄存器变量。寄存器变量比存储在内存中的变量访问效率更高(默认情况下,自动变量和静态变量都是放在内存中的)
2> 哪些变量是寄存器变量:
- 被关键字register修饰的自动变量都是寄存器变量
- 只有自动变量才可以是寄存器变量,全局变量和静态局部变量不行
-
寄存器变量只限于int、char和指针类型变量使用
3> 生命周期:因为寄存器变量本身就是自动变量,所以函数中的寄存器变量在调用该函数时占用寄存器中存放的值,当函数结束时释放寄存器,变量消失。
4> 使用注意:
- 由于计算机中寄存器数目有限,不能使用太多的寄存器变量。如果寄存器使用饱和时,程序将寄存器变量自动转换为自动变量处理
- 为了提高运算速度,一般会将一些频繁使用的自动变量定义为寄存器变量,这样程序尽可能地为它分配寄存器存放,而不用内存
~~~
1 int main() {
2 register int a;
3 return 0;
4 }
~~~
第2行的变量a是个寄存器变量。
';
C语言入门教程21-预处理指令3-文件包含
最后更新于:2022-04-01 20:26:54
本文目录
- [一、基本概念](http://www.cnblogs.com/mjios/archive/2013/03/20/2971575.html#label0)
- [二、一般形式](http://www.cnblogs.com/mjios/archive/2013/03/20/2971575.html#label1)
- [三、使用注意](http://www.cnblogs.com/mjios/archive/2013/03/20/2971575.html#label2)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
这讲介绍最后一个预处理指令---文件包含
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/20/2971575.html#labelTop)
### 一、基本概念
其实我们早就有接触文件包含这个指令了, 就是#include,它可以将一个文件的全部内容拷贝另一个文件中。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/20/2971575.html#labelTop)
### 二、一般形式
### 1.第1种形式#include <文件名>
直接到C语言库函数头文件所在的目录中寻找文件
### 2.第2种形式 #include "文件名"
系统会先在源程序当前目录下寻找,若找不到,再到操作系统的path路径中查找,最后才到C语言库函数头文件所在目录中查找
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/20/2971575.html#labelTop)
### 三、使用注意
1.#include指令允许嵌套包含,比如a.h包含b.h,b.h包含c.h,但是不允许递归包含,比如 a.h 包含 b.h,b.h 包含 a.h。
下面的做法是错误的
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7679125cb.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7679289df.png)
2.使用#include指令可能导致多次包含同一个头文件,降低编译效率
比如下面的情况:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767941501.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76795cb54.png)
在one.h中声明了一个one函数;在two.h中包含了one.h,顺便声明了一个two函数。(这里就不写函数的实现了,也就是函数的定义)
假如我想在main.c中使用one和two两个函数,而且有时候我们并不一定知道two.h中包含了one.h,所以可能会这样做:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76797a2b6.png)
编译预处理之后main.c的代码是这样的:
~~~
1 void one();
2 void one();
3 void two();
4 int main ()
5 {
6
7 return 0;
8 }
~~~
第1行是由#include "one.h"导致的,第2、3行是由#include "two.h"导致的(因为two.h里面包含了one.h)。可以看出来,one函数被声明了2遍,根本就没有必要,这样会降低编译效率。
为了解决这种重复包含同一个头文件的问题,一般我们会这样写头文件内容:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767996b47.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7679ae3d7.png)
大致解释一下意思,就拿one.h为例:当我们第一次#include "one.h"时,因为没有定义_ONE_H_,所以第9行的条件成立,接着在第10行定义了_ONE_H_这个宏,然后在13行声明one函数,最后在15行结束条件编译。当第二次#include "one.h",因为之前已经定义过_ONE_H_这个宏,所以第9行的条件不成立,直接跳到第15行的#endif,结束条件编译。就是这么简单的3句代码,防止了one.h的内容被重复包含。
这样子的话,main.c中的:
~~~
#include "one.h"
#include "two.h"
~~~
就变成了:
~~~
1 // #include "one.h"
2 #ifndef _ONE_H_
3 #define _ONE_H_
4
5 void one();
6
7 #endif
8
9 // #include "two.h"
10 #ifndef _TWO_H_
11 #define _TWO_H_
12
13 // #include "one.h"
14 #ifndef _ONE_H_
15 #define _ONE_H_
16
17 void one();
18
19 #endif
20
21 void two();
22
23 #endif
~~~
第2~第7行是#include "one.h"导致的,第10~第23行是#include "two.h"导致的。编译预处理之后就变为了:
~~~
1 void one();
2 void two();
~~~
这才是我们想要的结果
';
C语言入门教程20-预处理指令2-条件编译
最后更新于:2022-04-01 20:26:51
本文目录
- [条件编译的概念](http://www.cnblogs.com/mjios/archive/2013/03/20/2971275.html#label0)
- [一、基本用法](http://www.cnblogs.com/mjios/archive/2013/03/20/2971275.html#label1)
- [二、举个例子](http://www.cnblogs.com/mjios/archive/2013/03/20/2971275.html#label2)
- [三、其他用法](http://www.cnblogs.com/mjios/archive/2013/03/20/2971275.html#label3)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
上一篇已经介绍了[预处理指令中的宏定义](http://www.cnblogs.com/mjios/archive/2013/03/20/2969817.html),这篇就介绍一下条件编译
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/20/2971275.html#labelTop)
### 条件编译的概念
在很多情况下,我们希望程序的其中一部分代码只有在满足一定条件时才进行编译,否则不参与编译(只有参与编译的代码最终才能被执行),这就是条件编译。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/20/2971275.html#labelTop)
### 一、基本用法
~~~
1 #if 条件1
2 ...code1...
3 #elif 条件2
4 ...code2...
5 #else
6 ...code3...
7 #endif
~~~
1> 如果条件1成立,那么编译器就会把#if 与 #elif之间的code1代码编译进去(注意:是编译进去,不是执行,很平时用的if-else是不一样的)
2> 如果条件1不成立、条件2成立,那么编译器就会把#elif 与 #else之间的code2代码编译进去
3> 如果条件1、2都不成立,那么编译器就会把#else 与 #endif之间的code3编译进去
4> 注意,条件编译结束后,要在最后面加一个#endif,不然后果很严重(自己思考一下后果)
5> #if 和 #elif后面的条件一般是判断宏定义而不是判断变量,因为条件编译是在编译之前做的判断,宏定义也是编译之前定义的,而变量是在运行时才产生的、才有使用的意义
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/20/2971275.html#labelTop)
### 二、举个例子
~~~
1 #include
2
3 #define MAX 11
4
5 int main ()
6 {
7 #if MAX == 0
8 printf("MAX是0");
9 #elif MAX > 0
10 printf("MAX大于0");
11 #else
12 printf("MAX小于0");
13 #endif
14 return 0;
15 }
~~~
在第3行定义了一个宏MAX,当然在开发中这个MAX可能被定义在其他头文件中,现在只是为了方便演示,就写到main函数上面了。注意第7到第13行的条件编译语句。
由于MAX为11,所以#elif的条件成立,第10行代码将会被编译进去,其实编译预处理后的代码是这样的:
~~~
1 /*stdio.h文件中的内容将会代替#include 的位置*/
2
3 int main ()
4 {
5 printf("MAX大于0");
6 return 0;
7 }
~~~
代码变得非常简洁,输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7678aecd8.png)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/20/2971275.html#labelTop)
### 三、其他用法
### 1.#if defined()和#if !defined()的用法
#if 和 #elif后面的条件不仅仅可以用来判断宏的值,还可以判断是否定义过某个宏。比如:
~~~
1 #if defined(MAX)
2 ...code...
3 #endif
~~~
如果前面已经定义过MAX这个宏,就将code编译进去。它不会管MAX的值是多少,只要定义过MAX,条件就成立。
条件也可以取反:
~~~
1 #if !defined(MAX)
2 ...code...
3 #endif
~~~
如果前面没有定义过MAX这个宏,就将code编译进去。
### 2.#ifdef和#ifndef的使用
#ifdef的使用和#if defined()的用法基本一致
~~~
1 #ifdef MAX
2 ...code...
3 #endif
~~~
如果前面已经定义过MAX这个宏,就将code编译进去。
#ifndef又和#if !defined()的用法基本一致
~~~
1 #ifndef MAX
2 ...code...
3 #endif
~~~
如果前面没有定义过MAX这个宏,就将code编译进去。
';
C语言入门教程19-预处理指令1-宏定义
最后更新于:2022-04-01 20:26:49
本文目录
- [预处理指令简介](http://www.cnblogs.com/mjios/archive/2013/03/20/2969817.html#label0)
- [一、不带参数的宏定义](http://www.cnblogs.com/mjios/archive/2013/03/20/2969817.html#label1)
- [二、带参数的宏定义](http://www.cnblogs.com/mjios/archive/2013/03/20/2969817.html#label2)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/20/2969817.html#labelTop)
### 预处理指令简介
1.C语言在对源程序进行编译之前,会先对一些特殊的预处理指令作解释(比如之前使用的#include文件包含指令),产生一个新的源程序(这个过程称为编译预处理),之后再进行通常的编译
2.为了区分预处理指令和一般的C语句,所有预处理指令都以符号"#"开头,并且结尾不用分号
3.预处理指令可以出现在程序的任何位置,它的作用范围是从它出现的位置到文件尾。习惯上我们尽可能将预处理指令写在源程序开头,这种情况下,它的作用范围就是整个源程序文件
4.C语言提供的预处理指令主要有:宏定义、文件包含、条件编译
这一讲先介绍一下宏定义,宏定义可以分为2种:不带参数的宏定义 和 带参数的宏定义。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/20/2969817.html#labelTop)
### 一、不带参数的宏定义
### 1.一般形式
#define 宏名 字符串
比如#define ABC 10
右边的字符串也可以省略,比如#define ABC
### 2.作用
它的作用是在编译预处理时,将源程序中所有"宏名"替换成右边的"字符串",常用来定义常量。
接下来写个程序根据圆的半径计算周长
~~~
1 #include
2
3 // 源程序中所有的宏名PI在编译预处理的时候都会被3.14所代替
4 #define PI 3.14
5
6 // 根据圆的半径计radius算周长
7 float girth(float radius) {
8 return 2 * PI *radius;
9 }
10
11 int main ()
12 {
13 float g = girth(2);
14
15 printf("周长为:%f", g);
16 return 0;
17 }
~~~
在第4行定义了一个叫PI的宏,在编译预处理之后,第8行中的2 * PI *radius就会变成2 * 3.14 * radius。
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7677eda07.png)
### 3.使用习惯与注意
1> 宏名一般用大写字母,以便与变量名区别开来,但用小写也没有语法错误
2> 对程序中用双引号扩起来的字符串内的字符,不进行宏的替换操作。比如:
~~~
1 #define R 10
2 int main ()
3 {
4 char *s = "Radio";
5 return 0;
6 }
~~~
在第1行定义了一个叫R的宏,但是第4行中"Radio"里面的'R'并不会被替换成10
3> 在编译预处理用字符串替换宏名时,不作语法检查,只是简单的字符串替换。只有在编译的时候才对已经展开宏名的源程序进行语法检查
~~~
1 #define I 100
2 int main ()
3 {
4 int i[3] = I;
5 return 0;
6 }
~~~
在做编译预处理的时候,不管语法对不对,第4行的I都会被替换为100。不过在编译的时候就会报第4行的错。
4> 宏名的有效范围是从定义位置到文件结束。如果需要终止宏定义的作用域,可以用#undef命令
~~~
1 #define PI 3.14
2 /*
3 .
4 .
5 .
6 .
7 */
8 #undef PI
~~~
PI这个宏在第1行到第8行之间是有效的,第8行后就无效了
5> 定义一个宏时可以引用已经定义的宏名
~~~
#define R 3.0
#define PI 3.14
#define L 2*PI*R
#define S PI*R*R
~~~
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/20/2969817.html#labelTop)
### 二、带参数的宏定义
### 1.一般形式
#define 宏名(参数列表) 字符串
### 2.作用
在编译预处理时,将源程序中所有宏名替换成字符串,并且将 字符串中的参数 用 宏名右边参数列表 中的参数替换
~~~
1 #include
2
3 #define average(a, b) (a+b)/2
4
5 int main ()
6 {
7 int a = average(10, 4);
8
9 printf("平均值:%d", a);
10 return 0;
11 }
~~~
第3行中定义了一个带有2个参数的宏average,第7行其实会被替换成:int a = (10 + 4)/2;,输出结果为:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76780a992.png)
。是不是感觉这个宏有点像函数呢?
### 3.使用注意
1> 宏名和参数列表之间不能有空格,否则空格后面的所有字符串都作为替换的字符串
~~~
1 #define average (a, b) (a+b)/2
2
3 int main ()
4 {
5 int a = average(10, 4);
6 return 0;
7 }
~~~
注意第1行的宏定义,宏名average跟(a, b)之间是有空格的,于是,第5行就变成了这样:
~~~
int a = (a, b) (a+b)/2(10, 4);
~~~
这个肯定是编译不通过的
2> 带参数的宏在展开时,只作简单的字符和参数的替换,不进行任何计算操作。所以在定义宏时,一般用一个小括号括住字符串的参数。
下面定义一个宏D(a),作用是返回a的2倍数值:
- 如果定义宏的时候不用小括号括住参数
-
~~~
1 #include
2
3 #define D(a) 2*a
4
5 int main ()
6 {
7 int b = D(3+4);
8
9 printf("%d", b);
10 return 0;
11 }
~~~
第7行将被替换成int b = 2*3+4;,输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76781a417.png)
- 如果定义宏的时候用小括号括住参数,把上面的第3行改成:
~~~
#define D(a) 2*(a)
~~~
注意右边的a是有括号的,第7行将被替换成int b = 2*(3+4);,输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76782b609.png)
3> 计算结果最好也用括号括起来
下面定义一个宏P(a),作用是返回a的平方:
- 如果不用小括号括住计算结果
~~~
1 #include
2
3 #define Pow(a) (a) * (a)
4
5 int main(int argc, const char * argv[]) {
6 int b = Pow(10) / Pow(2);
7
8 printf("%d", b);
9 return 0;
10 }
~~~
注意第3行,没有用小括号扩住计算结果,只是括住了参数而已。第6行代码被替换为:
~~~
int b = (10) * (10) / (2) * (2);
~~~
简化之后:int b = 10 * (10 / 2) * 2;,最后变量b为:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76783bd9d.png)
- 如果用小括号括住计算结果
将上面的第3行代码改为:
~~~
#define Pow(a) ( (a) * (a) )
~~~
那么第6行被替换为:
~~~
int b = ( (10) * (10) ) / ( (2) * (2) );
~~~
简化之后:int b = (10 * 10) / (2 * 2);,最后输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76784b820.png)
。这个才是我们想要的结果。
也就意味着前面的#define average(a, b) (a+b)/2应该写成#define average(a, b) (((a)+(b))/2)
### 5.与函数的区别
从整个使用过程可以发现,带参数的宏定义,在源程序中出现的形式与函数很像。但是两者是有本质区别的:
1> 宏定义不涉及存储空间的分配、参数类型匹配、参数传递、返回值问题
2> 函数调用在程序运行时执行,而宏替换只在编译预处理阶段进行。所以带参数的宏比函数具有更高的执行效率
';
C语言入门教程18-指针与字符串
最后更新于:2022-04-01 20:26:47
本文目录
- [字符串回顾](http://www.cnblogs.com/mjios/archive/2013/03/18/2965750.html#label0)
- [一、用指针遍历字符串的所有字符](http://www.cnblogs.com/mjios/archive/2013/03/18/2965750.html#label1)
- [二、用指针直接指向字符串](http://www.cnblogs.com/mjios/archive/2013/03/18/2965750.html#label2)
- [三、指针处理字符串的注意](http://www.cnblogs.com/mjios/archive/2013/03/18/2965750.html#label3)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/18/2965750.html#labelTop)
### 字符串回顾
一个字符串由一个或多个字符组成,因此我们可以用字符数组来存放字符串,不过在数组的尾部要加上一个空字符'\0'。
~~~
char s[] = "mj";
~~~
上面的代码定义了一个字符数组s来存储字符串"mj",系统会自动在尾部加上一个空字符'\0'。
内存分布大致如右图所示:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7676d3f00.png)
从上一篇文章《[十二、指向一维数组元素的指针](http://www.cnblogs.com/mjios/archive/2013/03/18/2964748.html)》中可以看出指针和数组的关系非常密切,因此我们也可以使用指针来操作字符串。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/18/2965750.html#labelTop)
### 一、用指针遍历字符串的所有字符
~~~
1 // 定义一个指针p
2 char *p;
3
4 // 定义一个数组s存放字符串
5 char s[] = "mj";
6
7 // 指针p指向字符串的首字符'm'
8 p = s; // 或者 p = &s[0];
9
10 for (; *p != '\0'; p++) {
11 printf("%c \n", *p);
12 }
~~~
执行完第8行后,内存分布如右图:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7676ec5b5.png)
有了前面[指针与数组](http://www.cnblogs.com/mjios/archive/2013/03/18/2964748.html)的基础相信大家能看到第9行之后的代码了:每次遍历之前先判断p当前指向的字符是否为空字符\0,如果不是空字符,就打印当前字符,然后执行p++让指针p指向下一个字符元素。
最后的输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76770dd97.png)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/18/2965750.html#labelTop)
### 二、用指针直接指向字符串
从前面可以看出,指针确实可以指向字符串并操作字符串。不过前面的做法是:先定义一个字符串数组存放字符串,然后将数组首地址传给指针p,让p指向字符串的首字符。
### 1.我们也可以直接用指针指向一个字符串,省略定义字符数组这个步骤
~~~
1 #include
2
3 int main()
4 {
5 // 定义一个字符串,用指针s指向这个字符串
6 char *s = "mj";
7
8 // 使用strlen函数测量字符串长度
9 int len = strlen(s);
10
11 printf("字符串长度:%D", len);
12 return 0;
13 }
~~~
注意第6行,我们直接用指针s指向了字符串"mj",并没有先创建一个字符数组。看第9行,将指针s传入到strlen函数中,说明之前所学习的字符串处理函数依然可以正常使用。输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76771cac2.png)
### 2.我们再来看看strlen函数在string.h中的声明
~~~
size_t strlen(const char *);
~~~
strlen函数中的形参是指向字符变量的指针类型,在《[十、字符和字符串常用处理函数](http://www.cnblogs.com/mjios/archive/2013/03/15/2961759.html)》中我们可以将一个字符数组名传进去,这一点又说明了指针与数组的密切关系,肯定有JQ。其实,调用strlen函数时,你传一个地址给它就行了,它会从这个地址开始计算字符的个数,直到遇到空字符'\0'位置,因此传入指针变量或者数组名都可以。
其他字符串处理函数也是一样的:
~~~
1 char *strcpy(char *, const char *); // 字符串拷贝函数
2 char *strcat(char *, const char *); // 字符串拼接函数
3 int strcmp(const char *, const char *); // 字符串比较函数
~~~
它们的参数都是指向字符变量的指针类型,因此可以传入指针变量或者数组名。
因此printf函数依然可以正常使用:
~~~
char *s = "mj";
printf("%s", s);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76772e389.png)
### 3.指针指向字符串的其他方式
~~~
char *s;
s = "mj";
~~~
上面的指向方式也是正确的:先定义指针变量,再指向字符串。如果是字符数组就不允许这样做,下面的做法是错误的:
~~~
1 char s[10];
2 s = "mj";
~~~
编译器肯定报第2行的错,因为s是个常量,代表数组的首地址,不能进行赋值运算。
还需要注意的是,下面的做法也是错误的:
~~~
1 char *s = "mj";
2
3 *s = "like";
~~~
第3行代码犯了2个错误:
- 第3行代码相当于把字符串"like"存进s指向的那一块内存空间,由第1行代码可以看出,s指向的是"mj"的首字符'm',也就是说s指向的一块char类型的存储空间,只有1个字节,要"like"存进1个字节的空间内,肯定内存溢出
- 由第1行代码可以看出,指针s指向的是字符串常量"mj"!因此是不能再通过指针来修改字符串内容的!就算是*s = 'A'这样"看起来似乎正确"的写法也是错误的,因为s指向的一个常量字符串,不允许修改它内部的字符。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/18/2965750.html#labelTop)
### 三、指针处理字符串的注意
现在想将字符串"lmj"的首字符'l'改为'L',解决方案是多种的
### 1.第一种方案
~~~
1 // 定义一个字符串变量"lmj"
2 char a[] = "lmj";
3
4 // 将字符串的首字符改为'L'
5 *a = 'L';
6
7 printf("%s", a);
~~~
程序正常运行,输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76773fcfe.png)
### 2.应该有人能马上想到第二种方案
~~~
1 char *p2 = "lmj";
2 *p2 = 'L';
3
4 printf("%s", p2);
~~~
看起来似乎是可行的,但这是错误代码,错在第2行。首先看第1行,指针变量p2指向的是一块字符串常量,正因为是常量,所以它内部的字符是不允许修改的。
有人可能搞蒙了,这里的第1行代码char *p2 = "lmj";跟第一种方案中的第2行代码char a[] = "lmj";不是一样的么?这是不一样的。
- char a[] = "lmj";定义的是一个字符串变量!
- char *p2 = "lmj";定义的是一个字符串常量!
';
C语言入门教程17-指向一维数组元素的指针
最后更新于:2022-04-01 20:26:44
本文目录
- [一、用指针指向一维数组的元素](http://www.cnblogs.com/mjios/archive/2013/03/18/2964748.html#label0)
- [二、用指针遍历数组元素](http://www.cnblogs.com/mjios/archive/2013/03/18/2964748.html#label1)
- [三、指针与数组的总结](http://www.cnblogs.com/mjios/archive/2013/03/18/2964748.html#label2)
- [四、数组、指针与函数参数](http://www.cnblogs.com/mjios/archive/2013/03/18/2964748.html#label3)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
前面我们已经学习了指针,如果指针存储了某个变量的地址,我们就可以说指针指向这个变量。数组及其数组元素都占有存储空间,都有自己的地址,因此指针变量可以指向整个数组,也可以指向数组元素。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/18/2964748.html#labelTop)
### 一、用指针指向一维数组的元素
~~~
1 // 定义一个int类型的数组
2 int a[2];
3
4 // 定义一个int类型的指针
5 int *p;
6
7 // 让指针指向数组的第0个元素
8 p = &a[0];
9
10 // 修改所指向元素的值
11 *p = 10;
12
13 // 打印第一个元素的值
14 printf("a[0] = %d", a[0]);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7675bd823.png)
,说明已经通过指针间接修改了数组元素的值,跟指向一个普通int类型变量是一样的。
由于数组名代表着数组的首地址,即a == &a[0],因此第8行代码等价于:
~~~
// 让指针指向数组的第0个元素
p = a;
~~~
内存分析图如下,一个指针变量占用2个字节,一个int类型的数组元素占用2个字节
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7675cc0fa.png)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/18/2964748.html#labelTop)
### 二、用指针遍历数组元素
### 1.最普通的遍历方式是用数组下标来遍历元素
~~~
1 // 定义一个int类型的数组
2 int a[4] = {1, 2, 3, 4};
3
4 int i;
5 for (i = 0; i < 4; i++) {
6 printf("a[%d] = %d \n", i, a[i]);
7 }
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7675e45f8.png)
### 2.接下来我们用指针来遍历数组元素
先定义一个指针,指向数组的第一个元素
~~~
// 定义一个int类型的数组
int a[4] = {1, 2, 3, 4};
// 定义一个int类型的指针,并指向数组的第0个元素
int *p = a;
~~~
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7676023cf.png)
p的值是a[0]的地址,因此,现在我们利用指针p只能访问数组的第0个元素a[0],用*p就可取出a[0]的值1。要想访问其他元素,就必须拿到元素的地址,可以发现每个元素的地址差值为2,因为在16位编译器环境下,一个int类型的变量占用2个字节。现在只是知道a[0]的地址值为p,怎么根据a[0]的地址获取其他元素的地址呢?其实非常简单,p+1就是a[1]的地址。注意了,这里的p+1代表着p的值加2,并不是p的值加1,比如p的值为ffc3,p+1则为ffc5,而非ffc4。依次类推,p+2就是a[2]的地址ffc7,p+3就是a[3]的地址ffc9。
#### 我先解释一下,为什么p+1代表p的值加2,而不是加1呢?
其实,p+1不一定代表p的值加2,也可能是加1、加4或者加8。究竟加多少,这跟指针的类型有关。下图是在16位编译器环境下的情况。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7676154f7.png)
聪明的你可能已经找到规律了,因为char类型的变量要占用1字节,所以p+1代表p的值加1;float类型的变量占用4字节,所以p+1代表p的值加4。从这一点,也可以很好地说明为什么指针一定要分类型,不同类型的指针,p+1的含义是不一样的。
上述代码中的p指向了int类型的数组元素a[0],所以p+1代表p的值加2。知道怎么获取其他元素的地址了,那么就可以利用指针p遍历数组元素了。
~~~
1 // 定义一个int类型的数组
2 int a[4] = {1, 2, 3, 4};
3
4 // 定义一个int类型的指针,并指向数组的第0个元素
5 int *p = a;
6
7 int i;
8 for (i = 0; i < 4; i++) {
9 // 利用指针运算符*取出数组元素的值
10 int value = *(p+i);
11
12 printf("a[%d] = %d \n", i, value);
13 }
~~~
注意第10行的代码,*(p+i)代表根据p+i的值(其实就是第i个数组元素的地址)访问对应的存储空间,并取出存储的内容(也就是取出第i个数组元素的值),赋值给左边的value。
最后的输出效果是一样的:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7675e45f8.png)
。注意的是:遍历完毕后,指针变量p还是指向a[0],因为p值一直没有变过,一直都是a[0]的地址ffc3。
补充一下,其实第10行改成下面的代码也是可以的:
~~~
int value = *(a+i);
~~~
大家都知道,a值代表数组的首地址,也就是a[0]的地址ffc3。a+1则代表a的值加2,即a[1]的地址ffc5,也就是说,a+i代表着元素a[i]的地址。相信大家也能猜出来了,a+1不一定代表着a值加2,究竟加多少,取决于数组的类型。a+i的计算方法与p+i相同。
利用上面的方法遍历完数组元素后,p一直指向元素a[0]。其实我们也可以直接修改p的值来访问数组元素,只需要改一下第10行的代码即可
~~~
// 利用指针运算符*取出数组元素的值
int value = *(p++);
~~~
p++其实就是相当于p = p + 1,直接修改了p值,而且每次是加2。因此,每执行一次p++,指针p就会指向下一个数组元素。
输出结果肯定是一样的:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7675e45f8.png)
。但是,遍历完毕后,指针变量p没有指向任何数组元素,因为一共执行了4次p++,最后p值为ffcb。当然,可以重新让p指向a[0]:p = &a[0];或者p = a;
注意,这里的写法是错误的
~~~
int value = *(a++);
~~~
a++相当于a=a+1,数组名a是个常量!不能进行赋值运算!
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/18/2964748.html#labelTop)
### 三、指针与数组的总结
p是指针,a是一个数组
1> 如果p指向了一个数组元素,则p+1表示指向数组该元素的下一个元素。比如,假设p = &a[0],则p+1表示a[1]的地址
2> 对于不同类型的数组元素,p值的改变是不同的。如果数组元素为int类型,p+1代表着p的值加上2(16位编译器环境下)
3> 如果p的初值是&a[0],那么
- p+i和a+i都可以表示元素a[i]的地址,它们都指向数组的第i个元素。a代表数组首地址,a+i也是地址,它的计算方法与p+i相同
-
*(p+i)和*(a+i)都表示数组元素a[i]
- 虽然p+i和a+i都指向数组的第i个元素,但二者使用时还是有区别的。因为作为指针变量的p可以改变自身值,如p++,使p的值自增。而数组名a是一个代表数组首地址的常量,它的值是不能改变的,即a++是不合法的
4> 引用一个数组元素可以有两种方法:
-
下标法: 如a[i]
-
指针法: 如*(p+i) 或 *(a+i)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/18/2964748.html#labelTop)
### 四、数组、指针与函数参数
1.用数组名作为函数实参时,是把实参数组的首地址传递给形参数组,两个数组共同占用同一段内存空间,这样形参数组中的元素值发生变化就会使实参数组的元素值也同时变化
~~~
1 void change(int b[]) {
2 b[0] = 10;
3 }
4
5 int main()
6 {
7 // 定义一个int类型的数组
8 int a[4] = {1, 2, 3, 4};
9
10 // 将数组名a传入change函数中
11 change(a);
12
13 // 查看a[0]
14 printf("a[0]=%d", a[0]);
15
16 return 0;
17 }
~~~
change函数的形参是数组类型的,在第11行调用change函数时,将数组名a,也就是数组的地址传给了数组b。因此数组a和b占用着同一块内存空间。
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767636bd5.png)
2.这种地址的传递也可以用指针来实现。函数的实参和形参都可以分别使用数组或指针。这样就有4种情况:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767647682.png)
也就是说,如果一个函数的形参类型是一个数组,调用函数时,你可以传入数组名或者指针变量;
~~~
1 void change(int b[]) {
2 b[0] = 10;
3 }
4
5 int main()
6 {
7 // 定义一个int类型的数组
8 int a[4] = {1, 2, 3, 4};
9
10 int *p = a;
11
12 // 将数组名a传入change函数中
13 change(p);
14
15 // 查看a[0]
16 printf("a[0]=%d", a[0]);
17
18 return 0;
19 }
~~~
注意第1行的形参类型是个数组int b[],第10行定义了指针变量p,第13行将p当做实参传入函数
如果一个函数的形参类型是一个指针变量,调用函数时,你可以传入数组名或者指针变量。
~~~
1 void change(int *b) {
2 b[0] = 10;
3 // 或者*b = 10;
4 }
5
6 int main()
7 {
8 // 定义一个int类型的数组
9 int a[4] = {1, 2, 3, 4};
10
11 // 将数组名a传入change函数中
12 change(a);
13
14 // 查看a[0]
15 printf("a[0]=%d", a[0]);
16
17 return 0;
18 }
~~~
注意第1行的形参类型是个指针变量int *b,第12行将数组名a当做实参传入函数。
由第2行可以看出,在很多情况下,指针和数组是可以相互切换使用的。但是,并不能说指针就等于数组。
';
C语言入门教程16-指针
最后更新于:2022-04-01 20:26:42
本文目录
- [直接引用](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#label0)
- [一、什么是指针?](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#label1)
- [二、指针的定义](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#label2)
- [三、指针的初始化](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#label3)
- [四、指针运算符](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#label4)
- [五、指针的用途举例](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#label5)
- [六、关于指针的疑问](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#label6)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
指针是C语言中非常重要的数据类型,如果你说C语言中除了指针,其他你都学得很好,那你干脆说没学过C语言。究竟什么是指针呢?我们先来看一个概念。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#labelTop)
### 直接引用
1. 回想一下,之前我们是如何更改某个变量的值?
我们之前是通过变量名来直接引用变量,然后进行赋值:
~~~
char a;
a = 10;
~~~
2. 看上去是很简单,其实程序内部是怎么操作的呢?
其实,程序对变量的读写操作,实际上是对变量所在的存储空间进行写入或取出数据。就上面的代码而言,系统会自动将变量名a转换为变量的存储地址,根据地址找到变量a的存储空间,然后再将数据10以2进制的形式放入变量a的存储空间中。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76733e868.png)
3. 通过变量名引用变量,由系统自动完成变量名和其存储地址之间的转换,称为变量的"直接引用"方式
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#labelTop)
### 一、什么是指针?
1.我们已经知道,"直接引用"是直接通过变量名来读写变量
2.C语言中还有一种"间接引用"的方式(以变量a为例):首先将变量a的地址存放在另一个变量中,比如存放在变量b中,然后通过变量b来间接引用变量a,间接读写变量a的值。这就是"间接引用"。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767354778.png)
如果程序通过"间接引用"的方式来修改a的值,可以这样做:先根据 变量名b 获取 变量b 的地址ffc2,取出变量b中存储的内容ffc1,也就是变量a的地址,再根据变量a的地址ffc1找到a的存储空间,然后修改里面的数据。
3.总结一句:用来存放变量地址的变量,就称为"指针变量"。在上面的情况下,变量b就是个"指针变量",我们可以说指针变量b指向变量a。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#labelTop)
### 二、指针的定义
一般形式:类名标识符 *指针变量名;
~~~
int *p;
float *q;
~~~
- "*"是一个说明符,用来说明这个变量是个指针变量,是不能省略的,但它不属于变量名的一部分
- 前面的类型标识符表示指针变量所指向的变量的类型,而且只能指向这种类型的变量
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#labelTop)
### 三、指针的初始化
### 1.先定义后初始化
~~~
1 // 定义int类型的变量a
2 int a = 10;
3
4 // 定义一个指针变量p
5 int *p;
6
7 // 将变量a的地址赋值给指针变量p,所以指针变量p指向变量a
8 p = &a;
~~~
注意第8行,赋值给p的是变量a的地址&a
### 2.在定义的同时初始化
~~~
// 定义int类型的变量a
int a = 10;
// 定义一个指针变量p
// 并将变量a的地址赋值给指针变量p,所以指针变量p指向变量a
int *p = &a;
~~~
### 3.初始化的注意
指针变量是用来存放变量地址的,不要给它随意赋值一个常数。下面的写法是错误的
~~~
int *p;
p = 200; // 这是错误的
~~~
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#labelTop)
### 四、指针运算符
### 1.给指针指向的变量赋值
~~~
1 char a = 10;
2 printf("修改前,a的值:%d\n", a);
3
4 // 指针变量p指向变量a
5 char *p = &a;
6
7 // 通过指针变量p间接修改变量a的值
8 *p = 9;
9
10 printf("修改后,a的值:%d", a);
~~~
当程序刚执行完第5行代码时,内存中大概的分布情况是这样的
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7673681ea.png)
,a值是10,p值就是变量a的地址ffc3。
注意下第5、第8行,都有个"*",它们的含义是不一样的:
(1) 第5行的"*"只是用来说明p是个指针变量
(2) 第8行的"*"是一个指针运算符,这里的*p代表根据p值ffc3这个地址访问对应的存储空间,也就是变量a的存储空间,然后将右边的数值9写入到这个存储空间,相当于 a = 9;,于是内存中就变成这样了
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767380e96.png)
输出结果为:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7673a4ab5.png)
,可以发现,我们通过变量p间接修改了变量a的值。
### 2.取出指针所指向变量的值
指针运算符除了可以赋值之外,还可以用于取值
~~~
1 char a = 10;
2
3 char *p;
4 p = &a;
5
6 char value = *p;
7 printf("取出a的值:%d", value);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7673b5fd0.png)
,第6行中的*p的意思是:根据p值(即变量a的地址)访问对应的存储空间,并取出存储的内容(即取出变量a的值),赋值给value
### 3.使用注意
在指针变量没有指向确定地址之前,不要对它所指的内容赋值。下面的写法是错误的
~~~
int *p;
*p = 10; //这是错误的
~~~
应该在指针变量指向一个确定的变量后再进行赋值。下面的写法才是正确的
~~~
// 定义2个int型变量
int a = 6, b;
// 定义一个指向变量b的指针变量p
int *p;
p = &b;
// 将a的值赋值给变量b
*p = a;
~~~
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#labelTop)
### 五、指针的用途举例
### 1.例子1
前面我们通过指针变量p间接访问了变量a,在有些人看来,觉得指针变量好傻B,直接用变量名a访问变量a不就好了么,干嘛搞这么麻烦。别着急,接下来举个例子,让大家看看指针还能做什么事情。
现在有个要求:写一个函数swap,接收2个整型参数,功能是互换两个实参的值。
1> 如果没学过指针,你可能会这样写
~~~
1 void swap(char v1, char v2) {
2 printf("更换前:v1=%d, v2=%d\n", v1, v2);
3
4 // 定义一个中间变量
5 char temp;
6
7 // 交换v1和v2的值
8 temp = v1;
9 v1 = v2;
10 v2 = temp;
11
12 printf("更换后:v1=%d, v2=%d\n", v1, v2);
13 }
14
15 int main()
16 {
17 char a = 10, b = 9;
18 printf("更换前:a=%d, b=%d\n", a, b);
19
20 swap(a, b);
21
22 printf("更换后:a=%d, b=%d", a, b);
23 return 0;
24 }
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7673c8b96.png)
,虽然v1和v2的值被交换了,但是变量a和b的值根本就没有换过来。因为基本数据类型作为函数实参时,只是纯粹地将值传递给形参,形参的改变并不影响实参。
我们可以简要分析一下这个过程:
* 在第20行中,将变量a、b的值分别传递给了swap函数的两个形参v1、v2
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7673dd06d.png)
* 在第8行中,将v1的值赋值给了temp
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7673f293b.png)
* 在第9行中,将v2的值赋值给了v1
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7674160e9.png)
* 在第10行中,将temp的值赋值给了v2
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76742a9f2.png)
就这样,v1和v2的值被交换了,但是a和b的值一直都没有改变
2> 如果学了指针,就应该这样写
~~~
1 void swap(char *v1, char *v2) {
2 // 中间变量
3 char temp;
4
5 // 取出v1指向的变量的值
6 temp = *v1;
7
8 // 取出v2指向的变量的值,然后赋值给v1指向的变量
9 *v1 = *v2;
10
11 // 赋值给v2指向的变量
12 *v2 = temp;
13 }
14
15 int main()
16 {
17 char a = 10, b = 9;
18 printf("更换前:a=%d, b=%d\n", a, b);
19
20 swap(&a, &b);
21
22 printf("更换后:a=%d, b=%d", a, b);
23 return 0;
24 }
~~~
先看看输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76743d317.png)
,变量a和b的值终于换过来了。
解释一下:
(在16位编译器环境下,一个指针变量占用2个字节)
* 先注意第20行,传递是变量的地址。因此swap函数的形参v1指向了变量a,v2指向了变量b
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76744fe84.png)
* 第6行代码是取出v1指向的变量的值,也就是变量a的值:10,然后赋值给变量temp
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7674673fa.png)
* 第9行代码是取出v2指向的变量(变量b)的值,然后赋值给v1指向的变量(变量a)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76747bbbf.png)
* 第12行代码是将temp变量的值赋值给v2指向的变量(变量b)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76748f0d1.png)
相信你已经感受到指针的强大了,如果没有指针,在一个函数的内部根本改变不了外部的实参。
### 2.例子2
接下来再举一个指针的实用例子。默认情况下,一个函数只能有一个返回值,有了指针,我们可以实现函数有"多返回值"。
现在有个要求:写一个函数sumAndMinus,可以同时计算2个整型的和与差,函数执行完毕后,返回和与差(注意了,这里要返回2个值)
~~~
// 计算2个整型的和与差
int sumAndMinus(int v1, int v2, int *minus) {
// 计算差,并赋值给指针指向的变量
*minus = v1 - v2;
// 计算和,并返回和
return v1 + v2;
}
int main()
{
// 定义2个int型变量
int a = 6, b = 2;
// 定义2个变量来分别接收和与差
int sum, minus;
// 调用函数
sum = sumAndMinus(a, b, &minus);
// 打印和
printf("%d+%d=%d\n", a, b, sum);
// 打印差
printf("%d-%d=%d\n", a, b, minus);
return 0;
}
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7674a4a36.png)
,和与差都由同一个函数计算并返回出来。和是函数的直接返回值,差是通过函数的第3个指针参数间接返回。
因此有了指针,我们可以让函数有"无限个"返回值。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/16/2963645.html#labelTop)
### 六、关于指针的疑问
刚学完指针,都可能有一大堆的疑惑,这里我列出几个常见的疑惑吧。
### 1.一个指针变量占用多少个字节的内存空间?占用的空间是否会跟随所指向变量的类型而改变?
在同一种编译器环境下,一个指针变量所占用的内存空间是固定的。比如,在16位编译器环境下,任何一个指针变量都只占用2个字节,并不会随所指向变量的类型而改变。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7674b4242.png)
### 2.既然每个指针变量所占用的内存空间是一样的,而且存储的都是地址,为何指针变量还要分类型?而且只能指向一种类型的变量?比如指向int类型的指针、指向char类型的指针。
其实,我觉得这个问题跟"数组为什么要分类型"是一样的。
* 看下面的代码,利用指针p读取变量c的值
~~~
1 int i = 2;
2 char c = 1;
3
4 // 定义一个指向char类型的指针
5 char *p = &c;
6
7 // 取出
8 printf("%d", *p);
~~~
这个输出结果应该难不倒大家:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7674c6d0a.png)
,是可以成功读取的。
* 如果我改一下第5行的代码,用一个本应该指向int类型变量的指针p,指向char类型的变量c
~~~
int *p = &c;
~~~
我们再来看一下输出:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7674d6a7e.png)
,c的原值是1,现在取出来却是513,怎么回事呢?这个要根据内存来分析
根据变量的定义顺序,这些变量在内存中大致如下图排布:
其中,指针变量p和int类型变量i各占2个字节,char类型的c占一个字节,p指向c,因此p值就是c的地址
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7674e8a10.png)
1> 最初的时候,我们用char *p指向变量c。当利用*p来获取变量c的值时,由于指针p知道变量c是char类型的,所以会从ffc3这个地址开始读取1个字节的数据:0000 0001,转为10进制就是1
2> 后来,我们用int *p指向变量c。当利用*p获取变量c的值时,由于指针p认为变量c是int类型的,所以会从ffc3这个地址开始读取2个字节的数据:0000 0010 0000 0001,转为10进制就是513
可见,给指针分类是多么重要的一件事,而且一种指针最好只指向一种类型的变量,那是最安全的。
';
C语言入门教程15-字符与字符串常用处理函数
最后更新于:2022-04-01 20:26:40
本文目录
- [一、字符处理函数](http://www.cnblogs.com/mjios/archive/2013/03/15/2961759.html#label0)
- [二、字符串处理函数](http://www.cnblogs.com/mjios/archive/2013/03/15/2961759.html#label1)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/15/2961759.html#labelTop)
### 一、字符处理函数
下面介绍的两个字符处理函数都是在stdio.h头文件中声明的。
### 1.字符输出函数putchar
~~~
putchar(65); // A
putchar('A'); // A
int a = 65;
putchar(a); // A
~~~
上面的3种用法,输出的都是大写字母A。
* putchar一次只能输出一个字符,而printf可以同时输出多个字符
~~~
printf("%c %c %c", 'A', 'B', 'a');
~~~
### 2.字符输入函数getchar
~~~
char c;
c = getchar();
~~~
getchar会将用户输入的字符赋值给变量c。
* getchar函数可以读入空格、TAB,直到遇到回车为止。scanf则不能读入空格和TAB。
* getchar一次只能读入一个字符。scanf则可以同时接收多个字符。
* getchar还能读入回车换行符,这时候你要敲2次回车键。第1次敲的回车换行符被getchar读入,第2次敲的回车键代表输入结束。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/15/2961759.html#labelTop)
### 二、字符串处理函数
下面介绍的字符串处理函数都是在string.h头文件中声明的,使用前要包含这个头文件。
### 1.strlen函数
* 这个函数可以用来测量字符串的字符个数,不包括\0
~~~
1 int size = strlen("mj"); // 长度为2
2
3 char s1[] = "lmj";
4 int size1 = strlen(s1); // 长度为3
5
6 char s2[] = {'m', 'j', '\0', 'l', 'm', 'j', '\0'};
7 int size2 = strlen(s2); // 长度为2
~~~
看一下第7行,strlen函数会从s2的首地址开始计算字符个数,直到遇到空字符\0为止。因为s2的第1个\0之前只有mj这2个字符,所以长度为2。
### 2.strcpy函数
~~~
1 char s[10];
2 strcpy(s, "lmj");
~~~
strcpy函数会将右边的"lmj"字符串拷贝到字符数组s中。从s的首地址开始,逐个字符拷贝,直到拷贝到\0为止。当然,在s的尾部肯定会保留一个\0。
* 假设右边的字符串中有好几个\0,strcpy函数只会拷贝第1个\0之前的内容,后面的内容不拷贝
~~~
1 char s[10];
2 char c[] = {'m', 'j', '\0', 'l', 'm', 'j', '\0'};
3 strcpy(s, c);
~~~
最后字符串s中的内容为:mj
### 3.strcat函数
~~~
char s1[30] = "LOVE";
strcat(s1, "OC");
~~~
strcat函数会将右边的"OC"字符串拼接到s1的尾部,最后s1的内容就变成了"LOVEOC"
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7671e0358.png)
strcat函数会从s1的第1个\0字符开始连接字符串,s1的第1个\0字符会被右边的字符串覆盖,连接完毕后在s1的尾部保留一个\0
* 注意下面的情况
~~~
1 char s1[30] = {'L', 'm', 'j', '\0', 'L', 'o', 'v', 'e', '\0'};
2 strcat(s1, "OC");
3 printf("%s", s1);
~~~
第1行初始化的s1有2个\0,经过第2行的strcat函数后,输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7671f34bb.png)
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76721044c.png)
### 4.strcmp函数
* 这个函数可以用来比较2个字符串的大小
* 调用形式为:strcmp(字符串1, 字符串2)
* 两个字符串从左至右逐个字符比较(按照字符的ASCII码值的大小),直到字符不相同或者遇见'\0'为止。如果全部字符都相同,则返回值为0。如果不相同,则返回两个字符串中第一个不相同的字符ASCII码值的差。即字符串1大于字符串2时函数返回值为正,否则为负。
~~~
1 char s1[] = "abc";
2 char s2[] = "abc";
3 char s3[] = "aBc";
4 char s4[] = "ccb";
5
6 printf("%d, %d, %d", strcmp(s1, s2), strcmp(s1, s3), strcmp(s1, s4));
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767223bb6.png)
- s1和s2相同,所以返回0
- s1和s3是第2个字符不相同,b的ASCII码值是98,B的ASCII码值是66,b - B = 32,所以返回32
- s1和s4是第1个字符就不相同,a的ASCII码值是97,c的ASCII码值是99,a - c = -2,所以返回-2
';
C语言入门教程14-字符串
最后更新于:2022-04-01 20:26:38
本文目录
- [一、字符串简介](http://www.cnblogs.com/mjios/archive/2013/03/15/2961273.html#label0)
- [二、字符串的初始化](http://www.cnblogs.com/mjios/archive/2013/03/15/2961273.html#label1)
- [三、字符串的输出](http://www.cnblogs.com/mjios/archive/2013/03/15/2961273.html#label2)
- [ 四、字符串的输入](http://www.cnblogs.com/mjios/archive/2013/03/15/2961273.html#label3)
- [五、字符串数组](http://www.cnblogs.com/mjios/archive/2013/03/15/2961273.html#label4)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/15/2961273.html#labelTop)
### 一、字符串简介
* 在Java中,一个字符串可以用String类型来存储
~~~
String s = "MJ";
~~~
C语言中没有String这种类型。其实字符串就是字符序列,由多个字符组成,所以在C语言中,我们可以用字符数组来存储字符串。
* 字符串可以看做是一个特殊的字符数组,为了跟普通的字符数组区分开来,应该在字符串的尾部添加了一个结束标志'\0'。'\0'是一个ASCII码值为0的字符,是一个空操作符,表示什么也不干。所以采用字符数组存放字符串,赋值时应包含结束标志'\0'。
* 字符串"mj"的存储情况如下(假设用字符数组char a[]来存储):
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76711d263.png)
注意了,尾部有个'\0',如果没有这个结束标记,说明这个字符数组存储的并不是字符串
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/15/2961273.html#labelTop)
### 二、字符串的初始化
~~~
1 char a[3] = {'m', 'j', '\0'};
2
3 char b[3];
4 b[0] = 'm';
5 b[1] = 'j';
6 b[2] = '\0';
7
8 char c[3] = "mj";
9
10 char d[] = "mj";
11
12 char e[20] = "mj";
~~~
当我们使用类似第8行的初始化方式时,系统会自动在字符串尾部加上一个\0结束符
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/15/2961273.html#labelTop)
### 三、字符串的输出
我们可以使用stdio.h中两个函数来输出字符串,分别是printf和puts函数
### 1.printf函数
* 这个函数我们已经用过很多遍了,用格式符%s表示需要输出一个字符串
~~~
char a[3] = {'m', 'j', '\0'};
printf("%s", a);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76712c64d.png)
,最后面那个\0是不可能输出的,它只是个空字符,只是字符串结束的标记。
* 说到这里,有人可能会想:这样看来,似乎把最后的\0去掉也没什么影响吧,输出结果应该还是一样的啊,都是"mj"。
我们可以试一下,把最后面的\0去掉,再进行输出:
~~~
char a[3] = {'m', 'j'};
printf("%s", a);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76712c64d.png)
,跟上面添加了\0的输出结果是一样的。
别高兴地太早了,我只能说你这是侥幸一样的,运气好了一点。
* 我们再来看一个例子
~~~
1 char a[3] = {'m', 'j', '\0'}; // 添加了结束符\0
2
3 char b[] = {'i', 's'}; // 假设忘记添加结束符\0
4
5 printf("字符串a:%s", a); // 输出字符串a
6
7 printf("\n"); // 换行
8
9 printf("字符串b:%s", b); // 输出字符串b
~~~
看清楚了,第3行的字符数组b后面没有添加结束符\0,因此b不算是个正宗的字符串。
按照你的猜想,字符串b的输出应该就是"is",但是输出结果为:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7671426c2.png)
,可以看出,当我们尝试输出b的时候,把a也输出了。
要搞清楚为什么,首先要看看a和b的内存地址:
~~~
printf("a的地址:%x", a);
printf("\n");
printf("b的地址:%x", b);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7671540fe.png)
,由这个数据我们可以分析出a和b的内存存储情况如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7671687f3.png)
可以看出来,数组b和a的内存地址是连续的。我们再回到输出b的代码:
~~~
printf("字符串b:%s", b); // 输出字符串b
~~~
%s表示期望输出一个字符串,因此printf函数会从b的首地址开始按顺序输出字符,一直到\0字符为止,因为\0是字符串的结束标记。
所以,如果想要创建一个字符串,记得加上结束符\0,不然后果很严重,会访问到一些垃圾数据。
### 2.puts函数
~~~
1 char a[] = "mj";
2 puts(a);
3
4 puts("lmj");
~~~
看第2行代码,puts函数会从a的首地址开始输出字符,一直到\0字符为止。
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76717ed83.png)
,可以看出puts函数输出一个字符串后会自动换行。
* puts函数一次只能输出一个字符串,printf函数则可以同时输出多个字符串
~~~
printf("%s - %s", "mj", "lmj");
~~~
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/15/2961273.html#labelTop)
### 四、字符串的输入
stdio.h中有2个函数可以用来接收用户输入的字符串,分别是scanf和gets
### 1.scanf函数
~~~
char a[10];
scanf("%s", a);
~~~
scanf函数会从a的首地址开始存放用户输入的字符,存放完毕后,系统会自动在尾部加上一个结束标记\0
注意,不要写成scanf("%s", &a),因为a已经代表了数组的地址,没必要再加上&这个地址运算符。
### 2.gets函数
~~~
char a[10];
gets(a);
~~~
gets跟scanf一样,会从a的首地址开始存放用户输入的字符,存放完毕后,系统会自动在尾部加上一个结束标记\0。
* gets一次只能读取一个字符串,scanf则可以同时读取多个字符串
* gets可以读入包含空格、tab的字符串,直到遇到回车为止;scanf不能用来读取空格、tab
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/15/2961273.html#labelTop)
### 五、字符串数组
### 1.字符串数组简介
* 一维字符数组中存放一个字符串,比如一个名字char name[20] = "mj"
* 如果要存储多个字符串,比如一个班所有学生的名字,则需要二维字符数组,char names[15][20]可以存放15个学生的姓名(假设姓名不超过20字符)
* 如果要存储两个班的学生姓名,那么可以用三维字符数组char names[2][15][20]
### 2.字符串数组的初始化
~~~
char names[2][10] = { {'J','a','y','\0'}, {'J','i','m','\0'} };
char names2[2][10] = { {"Jay"}, {"Jim"} };
char names3[2][10] = { "Jay", "Jim" };
~~~
可以把字符串数组看作是一维数组,它的元素是字符串。字符串数组names由字符串"Jay"和字符串"Jim"构成。
';
C语言入门教程13-数组-批量数据存储
最后更新于:2022-04-01 20:26:35
本文目录
- [地址](http://www.cnblogs.com/mjios/archive/2013/03/15/2961147.html#label0)
- [一、一维数组](http://www.cnblogs.com/mjios/archive/2013/03/15/2961147.html#label1)
- [二、二维数组](http://www.cnblogs.com/mjios/archive/2013/03/15/2961147.html#label2)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
为了让大家更好地学习和理解数组,我们先来认识一下内存中的"地址"。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/15/2961147.html#labelTop)
### 地址
1.计算机中的内存是以字节为单位的存储空间。内存的每一个字节都有一个唯一的编号,这个编号就称为地址。凡存放在内存中的程序和数据都有一个地址,也就是说,一个函数也有自己的内存地址。
2.当定义一个变量时,系统就分配一个带有唯一地址的存储单元来存储这个变量。比如:
~~~
char a = 'A'; // A的ASCII值为65
int b = 66;
~~~
在16bit编译器环境下,系统为a、b分别分配1个字节、2个字节的存储单元。变量存储单元的第一个字节的地址就是该变量的地址。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7670058cc.png)
可以看出,变量a的地址是ffc3;变量b的地址是ffc1。内存中存储的都是2进制数据。
3.在调试过程中,我们采取打印的方式查看变量的地址:
~~~
int c = 10;
// 以16进制形式输出地址
printf("16进制:%x\n", &c);
// 以10进制形式输出地址
printf("10进制:%d", &c);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767017139.png)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/15/2961147.html#labelTop)
### 一、一维数组
### 1.一维数组的定义
* 定义的形式为:类型 数组名[元素个数]
~~~
int a[5];
~~~
* 只能放在数组名的后面,下面的都是错误写法:
~~~
int[5] a; // 错误
int[] b; // 错误
~~~
* 里面的个数必须是一个固定值,可以是常量(比如6、8)、常量表达式(比如3+4、5*7)。绝对不能使用变量或者变量表达式来表示元素个数,大多数情况下不要省略元素个数(当数组作为函数的形参和数组初始化时除外)
下面的都是正确写法:
~~~
int a[5]; // 整型常量
int b['A']; // 字符常量,其实就是65
int c[3*4]; // 整型常量表达式
~~~
下面的都是错误写法:
~~~
int a[]; // 没有指定元素个数,错误
int i = 9;
int a[i]; // 用变量做元素个数,错误
~~~
### 2.一维数组的存储
定义数组时,系统将按照数组类型和个数分配一段连续的存储空间来存储数组元素,如int a[3]占据了连续的6字节存储空间(在16位编译器环境下,一个int类型占用2个字节)。要注意的是,数组名代表着整个数组的地址,也就是数组的起始地址。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76702c4db.png)
注意:其实a不算是变量,是个常量,它代表着数组的地址。上图把a放到变量一栏是为了方便大家理解数组结构。
数组a的地址是ffc1,a[0]的地址是ffc1,a[1]的地址是ffc3,a[2]的地址是ffc5。因此a == &a[0],即第一个元素的地址就是整个数组的地址
### 3.一维数组的初始化
* 初始化的一般形式是:类型 数组名[元素个数] = {元素1, 元素2, ...};
~~~
int a[2] = {8, 10};
~~~
其实相当于:
~~~
int a[2];
a[0] = 8;
a[1] = 10;
~~~
注意的是:C语言中编译器是不会对数组下标越界进行检查的,所以自己访问数组元素时要小心
* 元素值列表可以是数组所有元素的初值,也可以是前面部分元素的初值
~~~
int a[4] = {2, 5};
~~~
当数组为整型时,初始化未确定初值的元素,默认为0,所以上面的a[2]、a[3]都为0
* 当对全部数组元素都赋初值时,可以省略元素个数
~~~
int a[] = {2, 5, 7};
~~~
说明数组a的元素个数是3
* 数组初始化时的赋值方式只能用于数组的定义,定义之后只能一个元素一个元素地赋值
下面的写法是错误的:
~~~
1 int a[3];
2 a[3] = {1, 2, 3}; // 错误
3 a = {1, 2, 3}; // 错误
~~~
其实为什么是错误的写法呢?我们可以简要分析一下。
1> 第2行的a[3]代表着访问数组的第4个元素,首先这里已经是数组下标越界了;就算没有越界,给a[3]赋值时也应该赋一个int类型的整数,不应该是{}。
2> 第3行的a是数组名,代表着数组的地址,它是个常量!给常量赋值,那肯定错了!
### 4.一维数组与函数参数
如果忘记了实参和形参的意思,可以回看下《[四、函数](http://www.cnblogs.com/mjios/archive/2013/03/13/2957628.html)》这篇文章
* 一维数组的元素作为函数实参,与同类型的简单变量作为实参一样,是单向的值传递,即数组元素的值传给形参,形参的改变不影响实参
~~~
// b是test函数的形参(形式参数)
void test(int b) {
b = 9;
}
int main()
{
int a[3];
a[0] = 10;
printf("函数调用前的a[0]:%d\n", a[0]);
test(a[0]); // a[0]是test函数的实参(实际参数)
printf("函数调用后的a[0]:%d", a[0]);
return 0;
}
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76703eabf.png)
* 大家都知道,数组名代表着整个数组的地址,如果一维数组的名字作为函数实参,传递的是整个数组,即形参数组和实参数组完全等同,是存放在同一存储空间的同一个数组。这样形参数组修改时,实参数组也同时被修改了。形参数组的元素个数可以省略。
~~~
// b是test函数的形参(形式参数)
void test(int b[]) { // 也可以写int b[3]
b[0] = 9;
}
int main()
{
int a[3];
a[0] = 10;
printf("函数调用前的a[0]:%d\n", a[0]);
test(a); // a是test函数的实参(实际参数)
printf("函数调用后的a[0]:%d", a[0]);
return 0;
}
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767051e41.png)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/15/2961147.html#labelTop)
### 二、二维数组
### 1.二维数组的定义
定义形式:类型 数组名[行数][列数]
~~~
int a[2][3]; // 共2行3列,6个元素
~~~
### 2.二维数组的存储
* C语言把二维数组当作是一维数组的集合,即二维数组是一个特殊的一维数组:它的元素是一维数组。例如int a[2][3]可以看作由一维数组a[0]和一维数组a[1]组成,这两个一维数组都包含了3个int类型的元素
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7670624f0.png)
* 二维数组的存放顺序是按行存放的,先存放第一行的元素,再存放第2行的元素。例如int a[2][3]的存放顺序是:a[0][0] → a[0][1] → a[0][2] → a[1][0] → a[1][1] → a[1][2]
* 再来看看在内存中的存储情况,例如int a[2][2]
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d767072d32.png)
(注意:a[0]、a[1]也是数组,是一维数组,而且a[0]、a[1]就是数组名,因此a[0]、a[1]就代表着这个一维数组的地址)
1> 数组a的地址是ffc1,数组a[0]的地址也是ffc1,即a = a[0];
2> 元素a[0][0]的地址是ffc1,所以数组a[0]的地址和元素a[0][0]的地址相同,即a[0] = &a[0][0];
3> 最终可以得出结论:a = a[0] = &a[0][0],以此类推,可以得出a[1] = &a[1][0]
### 3.二维数组的初始化
* 按行进行初始化
~~~
int a[2][3] = { {2, 2, 3}, {3, 4, 5} };
~~~
* 按存储顺序进行初始化(先存放第1行,再存放第2行)
~~~
int a[2][3] = {2, 2, 3, 3, 4, 5};
~~~
* 对部分元素进行初始化
~~~
int a[2][3] = { {2}, {3, 4} };
int b[3][3] = { { }, { , , 2}, {1, 2, 3}};
~~~
* 如果只初始化了部分元素,可以省略行数,但是不可以省略列数
~~~
int a[][3] = {1, 2, 3, 4, 5, 6};
int a[][3] = {{1, 2, 3}, {3, 5}, {}};
~~~
有些人可能想不明白,为什么可以省略行数,但不可以省略列数。也有人可能会问,可不可以只指定行数,但是省略列数?
其实这个问题很简单,如果我们这样写:
~~~
int a[2][] = {1, 2, 3, 4, 5, 6}; // 错误写法
~~~
大家都知道,二维数组会先存放第1行的元素,由于不确定列数,也就是不确定第1行要存放多少个元素,所以这里会产生很多种情况,可能1、2是属于第1行的,也可能1、2、3、4是第一行的,甚至1、2、3、4、5、6全部都是属于第1行的
三维乃至更多维的数组就不再提及了,大家以此类推。
';
C语言入门教程12-scanf与printf输入输出函数
最后更新于:2022-04-01 20:26:33
本文目录
- [一、printf函数](http://www.cnblogs.com/mjios/archive/2013/03/14/2960199.html#label0)
- [二、scanf函数](http://www.cnblogs.com/mjios/archive/2013/03/14/2960199.html#label1)
说明:这个C语言专题,是学习iOS开发的前奏。也为了让有面向对象语言开发经验的程序员,能够快速上手C语言。如果你还没有编程经验,或者对C语言、iOS开发不感兴趣,请忽略
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/14/2960199.html#labelTop)
### 一、printf函数
这是在stdio.h中声明的一个函数,因此使用前必须加入#include ,使用它可以向标准输出设备(比如屏幕)输出数据
### 1.用法
1> printf(字符串)
~~~
printf("Hello, World!");
~~~
输出结果是:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766dc9c2d.png)
2> printf(字符串, 格式符参数)
~~~
1 // 使用常量作参数
2 printf("My age is %d\n", 26);
3
4 // 也可以使用变量
5 int age = 17;
6 printf("My age is %d", age);
~~~
* 格式符%d表示以有符号的十进制形式输出一个整型,格式符参数中的26和age会代替%d的位置。
* 第2行代码中的\n是个转义字符,表示换行,所以输出了第一句"My age is 26"后会先换行,再输出"My age is 27"
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766ddb109.png)
* 如果去掉第2行中的\n,将会是这样的效果
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766deb72e.png)
总结:左边字符串中格式符的个数 必须跟 右边格式符参数的个数一样;格式符的类型决定了格式符参数的类型,比如使用%d,说明对应的格式符参数必须是整型。
再举个例子:
~~~
printf("My age is %d and no is %d", 27, 1);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766e099e6.png)
### 2.常用的格式符及其含义
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766012eb2.png)
### 3.格式符还可以添加一些精细的格式控制
#### 1> 输出宽度
* 我们先看看默认的整型输出
~~~
printf("The price is %d.", 14);
~~~
输出结果(注意,后面是有个点的):![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766e21019.png)
* 如果我把%d换成%4d:
~~~
printf("The price is %4d.", 14);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766e32555.png)
,你会发现"is"跟"14"的距离被拉开了
%4d的意思是输出宽度为4,而"14"的宽度为2,因此多出2个宽度,多出的宽度就会在左边用空格填补,因此你会看到"14"左边多了2个空格;如果实际数值宽度比较大,比如用%4d输出宽度为6的"142434",那就会按照实际数值宽度6来输出。
~~~
printf("The price is %4d.", 142434);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766e44261.png)
,"142434"的输出宽度为6
* 如果换成%-4d
~~~
printf("The price is %-4d.", 14);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766e59701.png)
,你会发现"14"跟"."的距离被拉开了
%-4d表示输出宽度为4,如果比实际数值宽度大,多出的宽度会在右边用空格填补;如果4比实际数值宽度小,就按照实际数值的宽度来输出
#### 2> 浮点数的小数位数
* 我们先看下默认的浮点数输出
~~~
printf("My height is %f", 179.95f);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766e6a9bc.png)
,默认是输出6位小数
* 如果只想输出2位小数,把%f换成%.2f即可
~~~
printf("My height is %.2f", 179.95f);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766e79c35.png)
* 当然,可以同时设置输出宽度和小数位数
~~~
printf("My height is %8.1f", 179.95f);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766e9f6e4.png)
,输出宽度为8,保留1位小数
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/03/14/2960199.html#labelTop)
### 二、scanf函数
这也是在stdio.h中声明的一个函数,因此使用前必须加入#include 。调用scanf函数时,需要传入变量的地址作为参数,scanf函数会等待标准输入设备(比如键盘)输入数据,并且将输入的数据赋值给地址对应的变量
### 1.简单用法
~~~
1 printf("Please input your age:");
2
3 int age;
4 scanf("%d", &age);
5
6 printf("Your age is %d.", age);
~~~
* 运行程序,执行完第1行代码,控制台会输出一句提示信息:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766eb56bc.png)
* 执行到第4行的scanf函数时,会等待用户的键盘输入,并不会往后执行代码。scanf的第1个参数是"%d",说明要求用户以10进制的形式输入一个整数。这里要注意,scanf的第2个参数传递的不是age变量,而是age变量的地址&age,&是C语言中的一个地址运算符,可以用来获取变量的地址。
* 接着我们可以在提示信息后面输入个8:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766ecaffe.png)
(由于Xcode自身的问题,我们只能在控制台输入宽度为1的数据,如果想输入宽度大于1的数据,比如输入27,可以从别的地方复制个27,再粘贴到控制台)
* 输入完毕后,敲一下回车键,目的是告诉scanf函数我们已经输入完毕了,scanf函数会将输入的8赋值给age变量
* scanf函数赋值完毕后,才会往后执行代码,执行到第6行时,控制器会输出:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766edc801.png)
### 2.其他用法
#### 1> 用scanf函数接收3个数值,在这里,每个数值之间用中划线-隔开
~~~
1 int a, b, c;
2 scanf("%d-%d-%d", &a, &b, &c);
3
4 printf("a=%d, b=%d, c=%d", a, b, c);
~~~
* 注意第2行,3个%d之间是用中划线-隔开的,因此我们在每输入一个整数后都必须加个中划线-,比如这样输入![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766eed648.png)
,不然在给变量赋值的时候会出问题
* 所有的数值都输入完毕后敲回车键,scanf函数会依次给变量a、b、c赋值,接着输出![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766f0a528.png)
注意:数值之间的分隔符是任意的,不一定要用中划线-,可以是逗号、空格、星号*、井号#等等,甚至是英文字母
~~~
// 逗号,
scanf("%d,%d,%d", &a, &b, &c); // 输入格式:10,14,20
// 井号#
scanf("%d#%d#%d", &a, &b, &c); // 输入格式:10#14#20
// 字母x
scanf("%dx%dx%d", &a, &b, &c); // 输入格式:10x14x20
~~~
#### 2> 用scanf函数接收3个数值,每个数值之间用空格隔开
~~~
1 int a, b, c;
2 scanf("%d %d %d", &a, &b, &c);
3
4 printf("a=%d, b=%d, c=%d", a, b, c);
~~~
* 注意第2行,3个%d之间是用空格隔开的,我们在每输入一个整数后必须输入一个分隔符,分隔符可以是空格、tab、回车
- 用空格做分隔符![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766f1b3ef.png)
- 用tab做分隔符![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766f2d40a.png)
- 用回车做分隔符![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766f3efae.png)
';
C语言入门教程11-函数的声明定义
最后更新于:2022-04-01 20:26:31
本文目录
- [一、函数的声明](http://www.cnblogs.com/mjios/archive/2013/06/08/3093954.html#label0)
- [二、多源文件开发](http://www.cnblogs.com/mjios/archive/2013/06/08/3093954.html#label1)
- [三、#include](http://www.cnblogs.com/mjios/archive/2013/06/08/3093954.html#label2)
在上一讲中,简单介绍了[函数](http://www.cnblogs.com/mjios/archive/2013/05/16/3082932.html)的定义和使用,只要你想完成一个新功能,首先想到的应该是定义一个新的函数来完成这个功能。这讲继续介绍函数的其他用法和注意事项。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/08/3093954.html#labelTop)
### 一、函数的声明
### 1.在C语言中,函数的定义顺序是有讲究的:默认情况下,只有后面定义的函数才可以调用前面定义过的函数
~~~
1 int sum(int a, int b) {
2 return a + b;
3 }
4
5 int main()
6 {
7 int c = sum(1, 4);
8 return 0;
9 }
~~~
第5行定义的main函数调用了第1行的sum函数,这是合法的。如果调换sum函数和main函数的顺序,在标准的C编译器环境下是不合法的(不过在GCC编译器环境下只是一个警告)
### 2.如果想把函数的定义写在main函数后面,而且main函数能正常调用这些函数,那就必须在main函数的前面进行函数的声明
~~~
1 // 只是做个函数声明,并不用实现
2 int sum(int a, int b);
3
4 int main()
5 {
6 int c = sum(1, 4);
7 return 0;
8 }
9
10 // 函数的定义(实现)
11 int sum(int a, int b) {
12 return a + b;
13 }
~~~
在第11行定义了sum函数,在第2行对sum函数进行了声明,然后在第6行(main函数中)就可以正常调用sum函数了。
### 3.函数的声明格式
1> 格式
~~~
返回值类型 函数名 (参数1, 参数2, ...)
~~~
只要你在main函数前面声明过一个函数,main函数就知道这个函数的存在,就可以调用这个函数。而且只要知道函数名、函数的返回值、函数接收多少个参数、每个参数是什么类型的,就能够调用这个函数了,因此,声明函数的时候可以省略参数名称。比如上面的sum函数声明可以写成这样:
~~~
int sum(int, int);
~~~
究竟这个函数是做什么用的,还要看函数的定义。
2> 如果只有函数的声明,而没有函数的定义,那么程序将会在链接时出错
下面的写法是错误的:
~~~
1 int sum(int a, int b);
2
3 int main()
4 {
5
6 sum(10, 11);
7
8 return 0;
9 }
~~~
- 在第1行声明了一个sum函数,但是并没有对sum函数进行定义,接着在第6行调用sum函数
- 这个程序是可以编译成功的,因为我们在main函数前面声明了sum函数(函数的声明和定义是两码事),这个函数声明可以理解为:在语法上,骗一下main函数,告诉它sum函数是存在的,所以从语法的角度上main函数是可以调用sum函数的。究竟这个sum函数存不存在呢,有没有被定义呢?编译器是不管的。在编译阶段,编译器并不检测函数有没有定义,只有在链接的时候才会检测这个函数存不存在,也就是检测函数有没有被定义。
- 因此,这个程序会在链接的时候报错,错误信息如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766c5040c.png)
- 我这里的源文件是main.c文件,所以编译成功后生成一个main.o文件。链接的时候,链接器会检测main.o中的函数有没有被定义。
- 上面的错误信息大致意思是:在main.o文件中找不到sum这个标识符。
- 错误信息中的linker是链接器的意思,下次看到这个linker,说明是链接阶段出错了。链接出错了,就不能生成可执行文件,程序就不能运行。
- 这个错误的解决方案就是加上sum函数的定义。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/08/3093954.html#labelTop)
### 二、多源文件开发
### 1.为什么要有多个源文件
1> 在编写[第一个C程序](http://www.cnblogs.com/mjios/archive/2013/05/06/3062576.html)的时候已经提到:我们编写的所有C语言代码都保存在拓展名为.c的源文件中,编写完毕后就进行编译、链接,最后运行程序。
2> 在前面的学习过程中,由于代码比较少,因此所有的代码都保存在一个.c源文件中。但是,在实际开发过程中,项目做大了,源代码肯定非常多,很容易就上万行代码了,甚至上十万、百万都有可能。这个时候如果把所有的代码都写到一个.c源文件中,那么这个文件将会非常庞大,也非常恶心,你可以想象一下,一个文件有十几万行文字,不要说调试程序了,连阅读代码都非常困难。
3> 而且,公司里面都是以团队开发为主,如果多个开发人员同时修改一个源文件,那就会带来很多麻烦的问题,比如张三修改的代码很有可能会抹掉李四之前添加的代码。
4> 因此,为了模块化开发,一般会将不同的功能写到不同的.c源文件中,这样的话,每个开发人员都负责修改不同的源文件,达到分工合作的目的,能够大大提高开发效率。也就是说,一个正常的C语言项目是由多个.c源文件构成。
### 2.将sum函数写到其他源文件中
接下来就演示一下多个源文件的开发,我将前面定义的sum函数写在另一个源文件(命名为sum.c)中。这时候就有两个源文件:
#### 1> main.c文件
~~~
1 int main()
2 {
3
4 return 0;
5 }
~~~
#### 2> sum.c文件
~~~
1 int sum(int a, int b)
2 {
3 return a + b;
4 }
~~~
### 3.在main函数中调用sum函数
1> 现在想在main函数中调用sum函数,那么你可能会直接这样写:
~~~
1 int main()
2 {
3 int c = sum(10, 11);
4
5 return 0;
6 }
~~~
这种写法在标准C语言编译器中是直接报错的,因为main函数都不知道sum函数的存在,怎么可以调用它呢!!!
2> 我们应该骗一下main函数,sum函数是存在的,告诉它sum函数的返回值和参数类型即可。也就是说,应该在main函数前面,对sum函数进行声明。
main.c文件应该写成下面这样
~~~
1 #include
2
3 int sum(int, int);
4
5 int main()
6 {
7 int c = sum(10, 11);
8
9 printf("c is %d\n", c);
10
11 return 0;
12 }
~~~
注意第3行,加了一个sum函数的声明。为了检验sum函数的调用结果,在第9行用prinf函数将结果输出。
### 4.编译所有的源文件
sum.c和main.c都编写完毕后,就可以使用[gcc指令](http://www.cnblogs.com/mjios/archive/2013/05/06/3062576.html#label7)进行编译了。同时编译两个文件的指令是:cc -c main.c sum.c
编译成功后,生成了2个.o目标文件
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766c65184.png)
也可以单独编译:
cc -c main.c
cc -c sum.c
### 5.链接所有的目标文件
前面已经编译成功,生成了main.o和sum.o文件。现在应该把这2个.o文件进行链接,生成可执行文件。
1> 注意,一定要同时链接两个文件。如果你只是单独链接main.o或者sum.o都是不可能链接成功的。原因如下:
- 如果只是链接main.o文件:cc main.o,错误信息是:在main.o中找到不到sum这个标识符,其实就是找不到sum函数的定义。因为sum函数的定义在sum.o文件中,main.o中只有sum函数的声明
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766c7669d.png)
- 如果只是链接sum.o文件:cc sum.o,错误信息是:找不到main函数。一个C程序的入口点就是main函数,main函数定义在main.o中,sum.o中并没有定义main函数,连入口都没有,怎么能链接成功、生成可执行文件呢?
可以看出,main.o和sum.o有密不可分的关系,其实链接的目的就是将所有相关联的目标文件和C语言函数库组合在一起,生成可执行文件。
2> 链接main.o和sum.o文件:cc main.o sum.o,生成了可执行文件a.out
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766c8e1d0.png)
3> 运行a.out文件:./a.out,运行结果是在屏幕上输出了:
~~~
c is 21
~~~
说明函数调用成功,我们已经成功在main.c文件的main函数中调用了sum.c文件中的sum函数
4> 从中也可以得出一个结论:只要知道某个函数的声明,就可以调用这个函数,编译就能成功。不过想要这个程序能够运行成功,必须保证在链接的时候能找到函数的定义。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/08/3093954.html#labelTop)
### 三、#include
理解完前面的知识后,接下来就可以搞懂一个很久以前的问题:每次写在最前面的#include是干啥用的?
### 1.#include的作用
先来看一个最简单的C程序:
~~~
1 #include
2
3 int main()
4 {
5 printf("Hello, World!\n");
6 return 0;
7 }
~~~
这个程序的作用是在屏幕上输出Hello,World!这一串内容,我们主要关注第一行代码。
-#include 是C语言的预处理指令之一,所谓预处理,就是在编译之前做的处理,预处理指令一般以 # 开头
-#include 指令后面会跟着一个文件名,预处理器发现 #include 指令后,就会根据文件名去查找文件,并把这个文件的内容包含到当前文件中。被包含文件中的文本将替换源文件中的 #include 指令,就像你把被包含文件中的全部内容拷贝到这个 #include 指令所在的位置一样。所以第一行指令的作用是将stdio.h文件里面的所有内容拷贝到第一行中。
- 如果被包含的文件拓展名为.h,我们称之为"头文件"(Header File),头文件可以用来声明函数,要想使用这些函数,就必须先用 #include 指令包含函数所在的头文件
-#include 指令不仅仅限于.h头文件,可以包含任何编译器能识别的C/C++代码文件,包括.c、.hpp、.cpp等,甚至.txt、.abc等等都可以
也就是说你完全可以将第3行~第7行的代码放到其他文件中,然后用 #include 指令包含进来,比如:
1> 将第3行~第7行的代码放到my.txt中
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766ca10a1.png)
2> 在main.c源文件中包含my.txt文件
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766cb63ec.png)
- 编译链接后,程序还是可以照常运行的,因为 #include 的功能就是将文件内容完全拷贝到 #include 指令所在的位置
- 说明:这里用txt文件纯属演示,平时做项目不会这样做,除非吃饱了撑着,才会把代码都写到txt中去
### 2.#include可以使用绝对路径
上面的#include "my.txt"使用的是相对路径,其实也可以使用绝对路径。比如#include "/Users/apple/Desktop/my.txt"
### 3.#include <>和#include ""的区别
二者的区别在于:当被include的文件路径不是绝对路径的时候,有不同的搜索顺序。
1> 对于使用双引号""来include文件,搜索的时候按以下顺序:
- 先在这条include指令的父文件所在文件夹内搜索,所谓的父文件,就是这条include指令所在的文件
- 如果上一步找不到,则在父文件的父文件所在文件夹内搜索;
- 如果上一步找不到,则在编译器设置的include路径内搜索;
- 如果上一步找不到,则在系统的INCLUDE环境变量内搜索
2> 对于使用尖括号<>来include文件,搜索的时候按以下顺序:
- 在编译器设置的include路径内搜索;
- 如果上一步找不到,则在系统的INCLUDE环境变量内搜索
我这里使用的是clang编译器,clang设置include路径是(4.2是编译器版本):/usr/lib/clang/4.2/include
Mac系统的include路径有:
- /usr/include
- /usr/local/include
### 4.stdio.h
我们已经知道#include指令的作用了,可是为什么要在第一行代码包含stdio.h呢?
- [stdio.h](http://baike.baidu.com/view/538727.htm) 是C语言函数库中的一个头文件,里面声明了一些常用的输入输出函数,比如往屏幕上输出内容的printf函数
- 这里之所以包含 stdio.h 文件,是因为在第5行中用到了在 stdio.h 内部声明的printf函数,这个函数可以向屏幕输出数据,第7行代码输出的内容是:Hello, World!
- 注意:stdio.h里面只有printf函数的声明。前面已经提到:只要知道函数的声明,就可以调用这个函数,就能编译成功。不过想要这个程序能够运行成功,必须保证在链接的时候能找到函数的定义。其实链接除了会将所有的目标文件组合在一起,还会关联C语言的函数库,函数库中就有printf函数的定义。因此前面的程序是可以链接成功的。
### 5.头文件.h和源文件.c的分工
跟printf函数一样,我们在开发中会经常将函数的声明和定义写在不同的文件中,函数声明放在.h头文件中,函数定义放在.c源文件中。
下面我们将sum函数的声明和定义分别放在sum.h和sum.c中
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766cc55e6.png)
这是sum.h文件
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766cd6dda.png)
这是sum.c文件
然后在main.c中包含sum.h即可使用sum函数
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766ce6eec.png)
其实sum.h和sum.c的文件名不一样要相同,可以随便写,只要文件名是合法的。但还是建议写成一样,因为一看文件名就知道sum.h和sum.c是有联系的。
运行步骤分析:
1> 在编译之前,预编译器会将sum.h文件中的内容拷贝到main.c中
2> 接着编译main.c和sum.c两个源文件,生成目标文件main.o和sum.o,这2个文件是不能被单独执行的,原因很简单:
* sum.o中不存在main函数,肯定不可以被执行
* main.o中虽然有main函数,但是它在main函数中调用了一个sum函数,而sum函数的定义却存在于sum.o中,因此main.o依赖于sum.o
3> 把main.o、sum.o链接在一起,生成可执行文件
4> 运行程序
#### 说到这里,有人可能有疑惑:可不可以在main.c中包含sum.c文件,不要sum.h文件了?
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766d0679e.png)
大家都知道#include的功能是拷贝内容,因此上面的代码等效于:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766d2d8ff.png)
这么一看,语法上是绝对没有问题的,main.c、sum.c都能编译成功,分别生成sum.o、main.o文件。但是当我们同时链接main.o和sum.o时会出错。原因:当链接这两个文件时链接器会发现sum.o和main.o里面都有sum函数的定义,于是报"标识符重复"的错误,也就是说sum函数被重复定义了。默认情况下,C语言不允许两个函数的名字相同。因此,不要尝试去#include那些.c源文件。
#### 有人可能觉得分出sum.h和sum.c文件的这种做法好傻B,好端端多出2个文件,你把所有的东西都写到main.c不就可以了么?
- 没错,整个C程序的代码是可以都写在main.c中。但是,如果项目做得很大,你可以想象得到,main.c这个文件会有多么庞大,会严重降低开发和调试效率。
- 要想出色地完成一个大项目,需要一个团队的合作,不是一个人就可以搞的定的。如果把所有的代码都写在main.c中,那就导致代码冲突,因为整个团队的开发人员都在修改main.c文件,张三修改的代码很有可能会抹掉李四之前添加的代码。
- 正常的模式应该是这样:假设张三负责编写 main函数,李四负责编写其他自定义函数,张三需要用到李四编写的某个函数,怎么办呢?李四可以将所有自定义函数的声明写在一个.h文件中,比如 lisi.h,然后张三在他自己的代码中用#include包含lisi.h文件,接着就可以调用lisi.h中声明的函数了,而李四呢,可以独立地在另外一个文件中(比如lisi.c)编写函数的定义,实现那些在lisi.h中声明的函数。这样子,张三和李四就可以相互协作、不会冲突。
-
';
C语言入门教程10-函数
最后更新于:2022-04-01 20:26:29
本文目录
- [一、基本概念](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#label0)
- [二、函数的定义](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#label1)
- [三、形式参数和实际参数](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#label2)
- [四、返回值类型](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#label3)
- [五、return](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#label4)
- [六、函数定义的注意](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#label5)
- [七、常见函数](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#label6)
前面已经讲完了C语言中的基本语句和基本运算了,这讲呢,介绍C语言中的重头戏---函数。其实函数这个概念,在大部分高级语言中都是非常重要的,我也已经在《[第一个C语言程序](http://www.cnblogs.com/mjios/archive/2013/05/06/3062576.html)》一讲中对函数作了一个简单介绍。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#labelTop)
### 一、基本概念
### 1.什么是函数
任何一个C语言程序都是由一个或者多个程序段(小程序)构成的,每个程序段都有自己的功能,我们一般称这些程序段为“函数”。所以,你可以说C语言程序是由函数构成的。
比如你用C语言编写了一个MP3播放器程序,那么它的程序结构如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d60420e413.png)
- 从上图可以看出:函数就是用来实现某个功能的程序段,每一个函数都有自己的功能。因此,你应该把实现某个功能所需的代码都写在函数中。比如,有个函数的功能是播放MP3,那么播放MP3的代码都应该写到这个函数中。
- 当调用(执行)一个函数时,计算机就会按顺序执行函数中的所有代码,从而展示函数所实现的功能。
### 2.函数名
一般来说,我们会将不同的功能交给不同的函数去实现。比如,将暂停播放MP3的代码写到一个函数中,将停止播放MP3的代码写到另一个函数中。因此,一个C程序中可能会有很多的函数。为了方便调用和区分这些函数,我们需要为每一个函数都起一个唯一的名称,函数的命名必须按照[标识符](http://www.cnblogs.com/mjios/archive/2013/05/07/3064469.html)命名规则。利用函数名就可以调用到对应的函数。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#labelTop)
### 二、函数的定义
### 1.任何一个函数在使用之前都必须进行定义
定义函数的目的就是为了写清楚你这个函数是干啥用的,里面包含了哪些代码。函数的定义格式如下:
~~~
1 返回值类型 函数名(形式参数列表)
2 {
3 函数体
4 }
~~~
### 2.举例
比如定义一个函数用来计算两个整数的和。那么可以写成下面这样:
~~~
1 #include
2
3 int sum(int a, int b)
4 {
5 int c = a + b;
6 return c;
7 }
8
9 int main()
10 {
11 int d = sum(10, 11);
12
13 printf("d is %d", d);
14 return 0;
15 }
~~~
- 分别在第3行、第9行定义了sum函数、main函数,其中sum函数的作用是计算两个整数的和。
- 函数定义好后,并不会马上执行函数内部的代码,要有人调用它才会执行内部的代码。就好像你的手机虽然有打电话的功能,但是只有按了拨号键,才会执行打电话的功能。
- 程序运行时就会先调用main函数,按顺序执行第11~14行代码,因为main函数是程序的入口
- 第11行的sum(10, 11)是调用sum函数,sum函数被调用了,就会分配存储空间给形式参数列表中的所有参数,也就是第3行中的变量a和变量b,它们都是“形式参数”
- 第11行中的10、11称为“实际参数”,它们将分别赋值给变量a、变量b,也就是相当于
~~~
1 int a = 10;
2 int b = 11;
~~~
- 既然调用了sum函数,就会按顺序执行sum函数中的代码,也就是第5~6行代码。
- 第5行将a+b的值21赋值给了变量c,第6行使用return关键字将变量c返回给了函数调用者,也是说,第11行sum(10, 11)的值是21。那么变量d的值就是21。
- 第13行的输出结果为:
~~~
d is 21
~~~
- 第3~7行中的变量a、b、c都只有在调用sum函数时,才会分配存储空间,当sum函数执行完毕后,变量a、b、c就会被释放存储空间。因此,它们的作用域仅限于sum函数内部。
- 第3行sum左边的int称为“返回值类型”,第6行return关键字后面跟着的数值称为“返回值”,因此第6行的变量c就是返回值,返回值的数据类型应该跟“返回值类型”保持一致。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#labelTop)
### 三、形式参数和实际参数
### 1.基本概念
1> 形式参数:在定义函数时,函数名后面的小括号()中定义的变量称为形式参数,简称形参
2> 实际参数:在调用函数时传入的值称为实际参数,简称实参
### 2.调用函数时传递的实参个数 必须和 函数的形参个数必须保持一致
~~~
1 int sum(int a, int b, int c)
2 {
3 return a + b + c;
4 }
5
6 int main()
7 {
8 sum(10, 9, 7);
9 return 0;
10 }
~~~
第1行中sum函数中有3个形参,因此在第8行调用sum函数时,需要传入3个实参
### 3.当使用基本数据类型(char、int、float等)作为实参时,实参和形参之间只是值传递,修改形参的值并不影响到实参
~~~
1 #include
2
3 int test(char a)
4 {
5 a = 10;
6
7 return 0;
8 }
9
10 int main()
11 {
12 char b = 5;
13
14 test(b);
15
16 printf("b的值是%d", b);
17
18 return 0;
19 }
~~~
- 在第3行定义了test函数,只有一个形参a
- 在第14行调用了test函数,接着就会分配内存给变量a。这里将变量b当做实参,那么变量b的值将传递给变量a。这个时候,内存中大致如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766a7dad5.png)
(其实在内存中存储的是二进制数据,这里我写成10进制是为了直观性)
- 执行完第5行代码后,变量a的值变成了10,但是变量b的值依然是5
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766a8e7fc.png)
- 第16行代码的输出结果是:
~~~
b的值是5
~~~
因此,在函数内部修改了形参的值,并不会影响到外面的实参。
### 4.函数可以没有形参
定义一个函数时是可以没有形参的,比如下面的函数
~~~
1 #include
2
3 int test()
4 {
5 printf("调用了test函数");
6 return 0;
7 }
8
9 int main()
10 {
11 test();
12 return 0;
13 }
~~~
- 在第3行定义了test函数,可以发现,它的形参列表是空的,也就是没有形参
- 那么在第11行调用test函数时,就不用传递任何实参
- 其实,第9行定义的main函数也是没有形参的
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#labelTop)
### 四、返回值类型
### 1.返回值就是函数调用完后,返回给函数调用者的结果,用return关键字进行返回。定义函数时,要指明函数的返回值类型
~~~
1 double pi()
2 {
3 return 3.4;
4 }
5
6 int main()
7 {
8 double a = pi();
9 return 0;
10 }
~~~
- 第1行定义了一个pi函数,返回值类型是double,因此用return返回的数值应该是double类型的,这里返回的是3.14
- 第8行pi函数调用完毕后,函数调用者得到的值就是3.14,因此,变量a的值是3.14
### 2.一个函数可以没有返回值,如果没有返回值,应该用void表示返回值类型
~~~
1 #include
2
3 void test()
4 {
5 printf("调用了test函数");
6 return;
7 }
8
9 int main()
10 {
11 test();
12 return 0;
13 }
~~~
- 在第3行定义了一个test函数,它是没有返回值的,所以第6行的return后面没有跟上任何数值
- 在第11行调用了test函数
### 3.如果一个函数没有返回值,最后面的return语句可以省略
所以,上面的test函数可以简化成:
~~~
1 void test()
2 {
3 printf("调用了test函数");
4 }
~~~
### 4.如果一个函数没有明确写出返回值类型,那么代表这个函数的返回值类型是int
~~~
1 sum(int a, int b)
2 {
3 return a + b;
4 }
~~~
可以看到,第1行定义的sum函数没有明确写出返回值类型,那么这个sum函数的返回值类型就是int。因此,第3行return后面跟随的是int类型数据
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#labelTop)
### 五、return
### 1.return语句可以在函数内部返回一个值给函数调用者
~~~
1 int sum(int a, int b)
2 {
3 return a + b;
4 }
5
6 int main()
7 {
8 int c = sum(5, 8);
9 return 0;
10 }
~~~
第3行使用了return语句,将a+b的值返回给了函数调用者,因此第8行变量c的值是13
### 2.一个函数内部可以多次使用return语句,使用了return语句后,函数就会马上停止执行,return语句后面的代码就不再被执行
~~~
1 int max(int a, int b)
2 {
3 if (a>b)
4 {
5 return a;
6 }
7
8 return b;
9 }
~~~
- 这个max函数可以用来求出两个整数中的最大值
- 如果a大于b,就执行第5行代码,直接将a返回,函数就会停止执行。也就是说,后面的第8行代码就不会被执行
- 如果a不大于b,就执行第8行代码,将b返回
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#labelTop)
### 六、函数定义的注意
### 1.函数名不能重复
默认情况下,函数的名字必须唯一。下面的写法是错误的:
~~~
1 void test(int a)
2 {
3
4 }
5
6 int test()
7 {
8 return 10;
9 }
~~~
第1、6行定义的函数的名称都是test,编译器会直接报错
### 2.每一个函数都应该独立定义,不能嵌套定义
下面的写法是错误的:
~~~
1 int main()
2 {
3 void test()
4 {
5
6 }
7
8 return 0;
9 }
~~~
在第3行把test函数定义在了main函数内部,这是错误的写法
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/08/3082932.html#labelTop)
### 七、常见函数
### 1.main函数
从[第一个C语言程序](http://www.cnblogs.com/mjios/archive/2013/05/06/3062576.html)开始,就认识了这个main函数。main函数是整个C程序的入口,有了main函数,C程序才能运行成功,而且整个C程序中只能有一个main函数。
main函数的简单定义如下:
~~~
1 int main()
2 {
3
4 return 0;
5 }
~~~
它可以没有形式参数,返回值是int类型。它的返回值用于说明程序的退出状态:如果返回0,则代表程序正常退出,否则代表程序异常退出
### 2.printf函数
你可能已经注意到,平时一直在使用的printf其实也是一个函数,这是系统自带的函数
~~~
1 #include
2
3 int main()
4 {
5
6 printf("我要学习iOS开发");
7
8 return 0;
9 }
~~~
- 在第6行调用了printf函数,传入的参数是一个[字符串常量](http://www.cnblogs.com/mjios/archive/2013/05/07/3065522.html):"我要学习iOS开发"
- printf函数的作用是在屏幕上输出内容
- 注意第1行代码,如果你使用了printf函数,就要加上这一句,至于原因,会在后面的章节详细介绍
';
C语言入门教程9-流程控制
最后更新于:2022-04-01 20:26:26
本文目录
- [前言](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#label0)
- [一、顺序结构](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#label1)
- [二、选择结构1-if语句](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#label2)
- [三、选择结构2-switch语句](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#label3)
- [四、循环结构1-while循环](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#label4)
- [五、循环结构2-do while循环](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#label5)
- [六、循环结构3-for循环](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#label6)
- [七、break和continue](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#label7)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#labelTop)
### 前言
### 1.默认的运行流程
默认情况下,程序的运行流程是这样的:运行程序后,系统会按书写顺序执行程序中的每一行代码。比如下面的程序
~~~
1 #include
2
3 int main()
4 {
5
6 printf("Hello-1\n");
7 printf("Hello-2\n");
8 printf("Hello-3\n");
9
10 return 0;
11 }
~~~
程序运行后,会按顺序执行第6、7、8行语句,于是输出结果为:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76694ffcf.png)
### 2.其他运行流程
但很多时候,我们并不想要按照默认的运行流程去走,比如想在某个条件成立的情况下才执行某一段代码,否则不执行。比如微信的这个界面:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76675626c.png)
如果用户点击了注册按钮,我们就执行“跳转到注册界面”的代码;如果用户点击了登录按钮,我们就执行“跳转到登录界面”的代码。如果用户没做出任何操作,就不执行前面所说的两段代码。要想实现这种功能,那就要学会如何去控制程序的运行流程。
### 3.流程结构
为了方便我们控制程序的运行流程,C语言提供3种流程结构,不同的流程结构可以实现不同的运行流程。这3种流程结构分别是:
- 顺序结构:默认的流程结构。按照书写顺序执行每一条语句。
- 选择结构:对给定的条件进行判断,再根据判断结果来决定执行哪一段代码。
- 循环结构:在给定条件成立的情况下,反复执行某一段代码。
下面是这3种结构的流程图,大致预览一下即可
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7669661f0.png)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#labelTop)
### 一、顺序结构
顺序结构是3种结构中最简单的,也是默认的流程结构:程序中的语句是按照书写顺序执行的。在[文章开头](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#label0)开始列出的代码段,就是顺序结构,这里就不多介绍了。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#labelTop)
### 二、选择结构1-if语句
C语言中选择结构的实现方式有两种:if语句和switch语句。先来看下if语句的使用,而if语句的形式是有好多种的。
### 1.形式1
先来看看if语句最简单的形式
#### 1> 简介
~~~
1 if ( 条件 )
2 {
3 语句1;
4 语句2;
5 ....
6 }
~~~
如果if右边小括号()中的条件成立,也就是为“真”时,就会执行第2~6行大括号{}中的语句;如果条件为假,就不执行大括号{}中的语句。这里的if是关键字。
#### 2> 举例
~~~
1 int a = 7;
2
3 if ( a )
4 {
5 printf("条件a成立\n");
6 printf("a的值为真");
7 }
~~~
C语言规定所有非0值都为“真”,而a的值是7,因此第3行的条件是成立的,接着就会执行第5、6行代码。输出结果如下:
~~~
1 条件a成立
2 a的值为真
~~~
如果将a的值改为0,那么第3行的条件就不成立,就不会执行第5、6行代码
#### 3> 省略大括号{}
如果if后面大括号{}中只有一行代码时,可以省略大括号。形式如下:
~~~
if ( 条件 )
语句1;
~~~
注意:如果条件成立,只会执行if后面的第1条语句;如果条件不成立,就不会执行if后面的第1条语句。
~~~
1 int a = 7;
2
3 if ( a > 9 )
4 printf("aaa");
5 printf("bbb");
~~~
因为第3行的a>9是不成立的,所以不会执行第4行代码。而第5行代码跟if语句是没有任何练习的,因此,第5行代码照常执行。于是会看到屏幕上只输出:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76697d308.png)
。
由于第5行代码跟if语句是没有任何联系的,所以一般会把代码写成下面这样:
~~~
1 int a = 7;
2
3 if ( a > 9 )
4 printf("aaa");
5 printf("bbb");
~~~
为了保证代码的可读性,不建议省略大括号!!!
#### 4> 语句嵌套
if语句内部是可以嵌套其他if语句的,如下面的例子
~~~
1 int a = 7;
2
3 if ( a > 0 )
4 {
5 printf("a的值大于0\n");
6
7 if ( a<9 )
8 {
9 printf("a的值小于9");
10 }
11 }
~~~
第3行的a>0是成立的,因此会按顺序执行第4~11大括号中的代码。执行到第7行的时候,a<9也是成立的,因此会执行第9行代码。输出结果:
~~~
1 a的值大于0
2 a的值小于9
~~~
#### 5> 使用注意1
有些人习惯写完一行代码就在后面加个分号";",于是写if语句的时候,他们可能会这样写:
~~~
1 int a = 6;
2 if ( a>8 );
3 {
4 printf("a大于8");
5 }
~~~
如果第2行尾部的分号,其实一个分号也是一条语句,这个叫做“空语句”。第2行的a>8不成立,所以不会执行后面的“空语句”。而后面的大括号{}跟if语句是没有联系的,因此会正常执行,于是会看到输出:
~~~
a大于8
~~~
所以要非常小心,千万不要在if的小括号后面添加分号。
第3~5行的内容是一个独立的“代码块”:
~~~
1 {
2 printf("a大于8");
3 }
~~~
#### 6> 使用注意2
下面的写法从语法的角度看是对的
~~~
int a = 10;
if (a = 0) {
printf("条件成立");
} else {
printf("条件不成立");
}
~~~
上述代码是完全合理的,编译器不会报错,只是个警告而已。因为a为0,所以为"假",输出结果是:"条件不成立"
这里隐藏着很大的陷阱在:
假设你本来是想判断a是否为0,那么本应该写if (a == 0),若你误写成了if (a = 0),那将是一件非常可怕的事情,因为编译器又不报错,这样的BUG就难找了。因此,像a==0这样的表达式,最好写成0==a,若你误写成0=a,编译器会直接报错的。
~~~
// 不推荐
if (a == 0) {
}
// 推荐
if (0 == a) {
}
~~~
#### 7> 使用注意3
在C语言中,可以不保存关系运算的结果。因此,下面的写法是合法的:
~~~
1 int a = 10;
2 a > 10;
3 a == 0;
~~~
这里又是一个陷阱,假设你的本意是想给a赋值为0,那么本应该写a = 0; ,若你误写成a == 0; ,那将又是一个非常难找的BUG,因为编译器根本不会报错。在1993年的时候,这个BUG差点让一桩价值2000万美元的硬件产品生意告吹,因为如果这个BUG不解决,这个产品就没办法正常使用
### 2.形式2
if还可以跟关键字else一起使用
#### 1> 简介
~~~
1 if ( 条件 )
2 {
3 语句1;
4 }
5 else
6 {
7 语句2;
8 }
~~~
如果条件成立,就会执行if后面大括号{}中的语句;如果条件不成立,就会执行else后面大括号{}中的语句。总之,两个大括号中一定会有1个被执行,而且只能执行的1个。
为了减少代码行数,你也可以写成下面的格式:
~~~
1 if ( 条件 ) {
2 语句1;
3 } else {
4 语句2;
5 }
~~~
当然,也可以省略大括号,写成下面的格式:
~~~
1 if ( 条件 )
2 语句1;
3 else
4 语句2;
~~~
如果条件成立,就执行if后面的第1条语句;如果条件不成立,就执行else后面的第1条语句。但还是不建议省略大括号{}。
#### 2> 举例
~~~
1 int a = 10;
2 if ( a==0 ) {
3 printf("a等于0");
4 } else {
5 printf("a不等于0");
6 }
~~~
第2行的a==0不成立,所以会执行第5行代码,输出结果:
~~~
a不等于0
~~~
### 3.形式3
if和else还有一种比较复杂的用法
#### 1> 简介
~~~
1 if ( 条件1 )
2 {
3 语句1;
4 }
5 else if ( 条件2 )
6 {
7 语句2;
8 }
9 else if ( 条件3 )
10 {
11 语句3;
12 }
13 ...
14 else
15 {
16 其他语句;
17 }
~~~
- 如果条件1成立,就执行条件1后面大括号{}中的内容:第2~4行
- 如果条件1不成立,条件2成立,就执行条件2后面大括号{}中的内容:第6~8行
- 如果条件1、条件2都不成立,条件3成立,就执行条件3后面大括号{}中的内容:第10~12行
- 第13行的...表示可以有无限个else if
- 如果所有的条件都不成立,就会执行else后面大括号{}中的内容:第15~17行
注意:这么多大括号中,只有1个大括号内的代码会被执行。跟之前一样,所有的大括号都可以省略,但是不建议省略。必要的时候,最后面的else那一段(第14~17行)是可以省略的。
#### 2> 举例
~~~
1 int a = 10;
2 if ( a==0 ) {
3 printf("a等于0");
4 } else if( a>0 ) {
5 printf("a大于0");
6 } else {
7 printf("a小于0");
8 }
~~~
第2行中的a==0不成立,接着会检查第4行。第4行的a>0成立,因此会执行第5行代码。输出结果:
~~~
a大于0
~~~
如果a的值是负数,那么第2、4行的条件都不成立,于是就会执行第7行代码。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#labelTop)
### 三、选择结构2-switch语句
### 1.形式
先来看看switch语句的使用形式:
~~~
1 switch(整型表达式)
2 {
3 case 数值1:
4 语句1;
5 break;
6 case 数值2:
7 语句2;
8 break;
9 ... ...
10 case 数值n:
11 语句n;
12 break;
13 default :
14 语句n+1;
15 break;
16 }
~~~
- 当整型表达式的值等于“数值1”时,就会执行“语句1”,后面的break表示退出整个switch语句,也就是直接跳到第16行代码;
- 当整形表达式的值等于“数值2”时,就会执行“语句2”;后面的以此类推。如果在数值1~数值n中,没有一个值等于整型表达式的值,那么就会执行default中的语句n+1。
- 由于所有的case后面都有个break,因此执行完任意一个case中的语句后,都会直接退出switch语句
### 2.举例
~~~
1 int a = 10;
2
3 switch (a) {
4 case 0:
5 printf("这是一个0");
6 break;
7 case 5:
8 printf("这是一个5");
9 break;
10 case 10:
11 printf("这是一个10");
12 break;
13 default:
14 printf("什么也不是");
15 break;
16 }
~~~
因为a的值刚好等于第10行case后面的10,所以会执行第11行代码,输出结果:
~~~
这是一个10
~~~
### 3.break
break关键字的作用是退出整个switch语句。默认的格式中,每个case后面都有个break,因此执行完case中的语句后,就会退出switch语句。
1> 如果某个case后面没有break,意味着执行完这个case中的语句后,会按顺序执行后面所有case和default中的语句,直到遇到break为止
~~~
1 int a = 0;
2
3 switch (a) {
4 case 0:
5 printf("这是一个0\n");
6 case 5:
7 printf("这是一个5\n");
8 case 10:
9 printf("这是一个10\n");
10 break;
11 default:
12 printf("什么也不是\n");
13 break;
14 }
~~~
- 由于变量a的值等于第4行case后面的0,因此肯定会执行第5行代码。
- 由于case 0中没有break语句,就不会退出switch语句,继续往下执行代码。
- 由于a的值已经等于第4行case的值,接着不会再判断a的值是否等于其他case的值了,直接按顺序执行第7、9行代码。在第10行有个break,接着就会退出switch语句。
- 输出结果为:
~~~
1 这是一个0
2 这是一个5
3 这是一个10
~~~
如果把a的值改为5,输出结果为:
~~~
1 这是一个5
2 这是一个10
~~~
2> 在某些时候,我们确实没有必要在每一个case后面添加break。下面举一个例子:判断分数的优良中差等级(100分满分)。
~~~
1 int score = 77;
2
3 switch (score/10) {
4 case 10:
5 case 9:
6 printf("优秀");
7 break;
8
9 case 8:
10 printf("良好");
11 break;
12
13 case 7:
14 case 6:
15 printf("中等");
16 break;
17
18 default:
19 printf("差劲");
20 break;
21 }
~~~
- 当score的范围是90~100,score/10的值为10或9时,就会执行第6行代码,然后退出switch语句;
- 当score的范围是80~89,score/10的值为8时,就会执行第10行代码,然后退出switch语句;
- 当score的范围是60~79,score/10的值为7或6时,就会执行第15行代码,然后退出switch语句;
- 当score的范围并不是60~100,score/10的值并不在6~10范围内时,就会执行第19行代码,然后退出switch语句;
- score的值是77,所以score/10的值是7,输出结果:中等
### 4.在case中定义变量
有时候,我们可能会想在case中定义一些变量,这个时候,就必须用大括号{}括住case中的所有语句。
~~~
1 int a = 10;
2 int b = 4;
3
4 char op = '-';
5
6 switch (op)
7 {
8 case '+':
9 {
10 int sum = a + b;
11 printf("a+b=%d\n", sum);
12 break;
13 }
14
15 case '-':
16 {
17 int minus = a - b;
18 printf("a-b=%d\n", minus);
19 break;
20 }
21
22 default:
23 printf("不能识别的符号");
24 break;
25 }
~~~
在第10、17分别定义两个变量。输出结果:
~~~
a-b=6
~~~
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#labelTop)
### 四、循环结构1-while循环
假如要你在屏幕上重复输出10次Hello World,你会怎么做?简单,把下面的代码拷贝10份就行了。
~~~
1 printf("Hello World\n");
~~~
没错,把上次代码写10遍,确实能实现功能。但是这样的代码太垃圾了,有很多的重复的代码,这样会使得代码非常地臃肿,复用率低。因此,不建议这么做。
下次遇到像上面那样重复执行某个操作时,首先要想到的应该是循环结构。所谓循环,就是重复执行某一个操作,C语言中有多种方式可以实现循环结构。先来看看while循环。
### 1.形式
~~~
1 while ( 条件 )
2 {
3 语句1;
4 语句2;
5 ....
6 }
~~~
- 如果条件成立,就会执行循环体中的语句(“循环体”就是while后面大括号{}中的内容)。然后再次判断条件,重复上述过程,直到条件不成立就结束while循环
- while循环的特点:如果while中的条件一开始就不成立,那么循环体中的语句永远不会被执行
可以省略大括号{},但是只会影响到while后面的第一条语句。不建议省略大括号。
~~~
1 while ( 条件 )
2 语句1;
~~~
### 2.举例
在屏幕上重复输出10次Hello World,每输出一次的换行。
~~~
1 int count = 0;
2 while ( count < 10 )
3 {
4 printf("Hello World\n");
5
6 count++;
7 }
~~~
如果省略第6行的count++,count就一直是0,那么count<10一直都是成立的,这个while循环将会陷入“死循环”,一直在重复执行第4行代码。
### 3.注意
如果写成下面这样,也会让程序进入“死循环”
~~~
1 int count = 0;
2
3 while ( count < 10 );
4 {
5 printf("Hello World\n");
6
7 count++;
8 }
~~~
- 注意第3行,while后面不小心加了个分号; ,一个分号表示一条空语句。
- 可以看出:while循环只会影响到第3行的空语句,而第4~8行的代码块是不受while循环影响的
- 由于count是0,那么count<10一直都是成立的,程序将会一直重复执行第3行的空语句,陷入死循环。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#labelTop)
### 五、循环结构2-do while循环
形式如下:
~~~
1 do {
2 语句1;
3 语句2;
4 ...
5 } while (条件);
~~~
- 注意第5行,后面是加上一个分号;的
- 当执行到do-while循环时,首先会执行一遍循环体中的语句(“循环体”就是do后面大括号{}中的内容)。接着判断while中的条件,如果条件成立,就执行循环体中的语句。然后再次判断条件,重复上述过程,直到条件不成立就结束while循环
- do-while循环的特点:不管while中的条件是否成立,循环体中的语句至少会被执行一遍
- 其实do while循环的用法跟while循环是差不多的,这里就不举例子了。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#labelTop)
### 六、循环结构3-for循环
### 1.形式
for循环是所有循环结构中最复杂的。
~~~
1 for (语句1; 条件; 语句2) {
2 语句3;
3 语句4;
4 ...
5 }
~~~
- for循环开始时,会先执行语句1,而且在整个循环过程中只执行一次语句1
- 接着判断条件,如果条件成立,就会执行循环体中的语句(“循环体”就是for后面大括号{}中的内容)
- 循环体执行完毕后,接下来会执行语句2,然后再次判断条件,重复上述过程,直到条件不成立就结束for循环
### 2.举例
~~~
1 for (int i = 0; i<5; i++)
2 {
3 printf("%d ", i);
4 }
~~~
输出结果为:
~~~
0 1 2 3 4
~~~
需要注意的是:变量i的作用域是第1~4行。一旦离开了这个for循环,变量i就失效了。
### 3.补充
如果for循环的初始化语句和循环一次后执行的语句是由多条语句组成的,就用逗号,隔开
~~~
1 for (int x = 0, y =0; x<3; x++, y+=2)
2 {
3 printf("x=%d, y=%d \n", x, y);
4 }
~~~
输出结果:
~~~
x=0, y=0
x=1, y=2
x=2, y=4
~~~
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3075946.html#labelTop)
### 七、break和continue
接下来,介绍两个比较重要的语句:break和continue。
### 1.break
前面在switch语句中已经用到了break,它的作用是跳出switch语句。它也可以用在循环结构中,这时候它的作用是跳出整个循环语句。
#### 1> 举例
这里以for循环为例子,break也可以用在while循环、do-while循环中。
~~~
1 for (int i = 0; i<5; i++) {
2 printf("i=%d \n", i);
3
4 if (i>2) {
5 break;
6 }
7 }
~~~
上面代码的意思是当i>2时,就跳出整个for循环,也就是结束for循环,所以输出结果是:
~~~
i=0
i=1
i=2
i=3
~~~
#### 2> for循环嵌套
先来看一个for循环嵌套的例子,嵌套的意思就是:for循环内部又一个for循环
~~~
1 for (int x = 0; x<2; x++) {
2 for (int y = 0; y<2; y++) {
3 printf("x=%d, y=%d \n", x, y);
4 }
5 }
~~~
输出结果是:
~~~
1 x=0, y=0
2 x=0, y=1
3 x=1, y=0
4 x=1, y=1
~~~
这个时候如果在for循环中加入一个break,那么这个break究竟是跳出里面还是外面的for循环呢?
~~~
1 for (int x = 0; x<2; x++) {
2 for (int y = 0; y<2; y++) {
3 printf("x=%d, y=%d \n", x, y);
4
5 break;
6 }
7 }
~~~
注意第5行的break,这个break的作用是跳出里面的for循环,并非外面的for循环。所以输出结果是:
~~~
x=0, y=0
x=1, y=0
~~~
如果改变一下break的位置
~~~
1 for (int x = 0; x<2; x++) {
2 for (int y = 0; y<2; y++) {
3 printf("x=%d, y=%d \n", x, y);
4 }
5
6 break;
7 }
~~~
注意第6行的break,这个break的作用是跳出外面的for循环,并非里面的for循环。所以输出结果是:
~~~
x=0, y=0
x=0, y=1
~~~
规律已经很明显了:break只会影响它所在的那个for循环
### 2.continue
continue只能使用在循环结构中,它的作用是跳过这一次循环,直接进入下一次循环。
这里以for循环为例子,continue也可以用在while循环、do-while循环中。
~~~
1 for (int x = 0; x<10; x++) {
2 if (x%2==0) {
3 continue;
4 }
5
6 printf("x=%d \n", x);
7 }
~~~
注意第2行,当x%2==0,也就是当x是2的倍数时,就跳过这次循环,不执行第6行语句,直接进入下一次循环。输出结果:
~~~
1 x=1
2 x=3
3 x=5
4 x=7
5 x=9
~~~
跟break一样,continue只会影响它所在的那个for循环
';
C语言入门教程8-运算符
最后更新于:2022-04-01 20:26:24
本文目录
- [一、算术运算符](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#label0)
- [二、赋值运算符](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#label1)
- [三、自增运算符和自减运算符](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#label2)
- [四、sizeof](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#label3)
- [五、逗号运算符](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#label4)
- [六、关系运算符](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#label5)
- [七、逻辑运算符](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#label6)
- [八、三目运算符](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#label7)
- [九、位运算符](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#label8)
计算机的基本能力就是计算,所以一门程序设计语言的计算能力是非常重要的。C语言之所以无所不能,是因为它不仅有丰富的数据类型,还有强大的计算能力。C语言一共有34种运算符,包括了常见的加减乘除运算。这讲就对C语言中的运算符做一个详细介绍。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#labelTop)
##一、算术运算符
算术运算符非常地简单,就是小学数学里面的一些加减乘除操作。不过呢,还是有一些语法细节需要注意的。
### 1.加法运算符 +
~~~
1 int a = 10;
2
3 int b = a + 5;
~~~
在第3行利用加法运算符 + 进行了加法运算,再将和赋值给了变量b,最终变量b的值是15
### 2.减法运算符 或 负值运算符 -
~~~
1 int b = 10 - 5;
2
3 int a = -10;
~~~
1> 在第1行利用减法运算符 - 进行了减法运算,再将差赋值给了变量b,最终变量b的值是5
2> 在第3行中,这个 - 并不是什么减法运算符,而算是一个负值运算符,-10代表的是负十
### 3.乘法运算符 *
~~~
1 int b = 10 * 5;
~~~
注意:乘法运算符并不是x或者X,而是星号*。变量b最终的值是50。
### 4.除法运算符 /
~~~
1 double a = 10.0 / 4;
2 double b = 10 / 4;
3
4 printf("a=%f, b=%f \n", a, b);
~~~
注意:除法运算符并不是÷,而是一个正斜杠 /
1> 第1行中的10.0是浮点型,4是整型,因此会将4自动类型提升为浮点型后再进行运算,最后变量b的值是2.5
2> 第2行中的10和4都是整型,计算机中的运算有个原则:相同数据类型的值才能进行运算,而且运算结果依然是同一种数据类型。因此,整数除于整数,求出来的结果依然是整数,会损失小数部分。最后变量b的值是2。查看输出结果:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766700421.png)
3> 如果想让 整数除于整数 不损失精度的话,可以将某个整数强制转换为浮点型数据
~~~
1 double a = (double)10 / 4;
2
3 double b = 10 / (double)4;
4
5 double c = (double)10 / (double)4;
6
7 double d = (double) (10 / 4);
~~~
- 10和4之间只要有1个强转为浮点型数据即可,另外1个整数会自动类型提升为浮点型数据。因此,变量a、b、c的值都是2.5。
- 变量d的情况就不一样了,第7行代码的意思是先计算(10/4)的值,再将(10/4)的值强转为浮点型数据。(10/4)的值是2,将2强转为浮点型数据,那不也就是2么?所以,变量d的值是2
### 5.模运算符 或称 取余运算符 %
注意:这个%并不是除号÷,它是一个取余运算符,或者叫做模运算符。取余的意思是,取得两个整数相除之后的余数。比如,5除于2的余数是1,5除于3的余数是2。因此使用这个%有个原则:%两侧必须都为整数。下面的写法是错误的:
~~~
1 int a = 5.0 % 2;
~~~
编译器会直接报错,因为5.0并非整数。
#### 1> 正数取余
~~~
1 int a = 5 % 2;
2 int b = 2 % 5;
~~~
简单计算可得:变量a的值为1,变量b的值为2
#### 2> 负数取余
~~~
1 int a = -5 % 2;
2 int b = 5 % -2;
3 int c = -5 % -2;
~~~
利用%求出来的余数是正数还是负数,由%左边的被除数决定,被除数是正数,余数就是正数,反之则反。因此变量a、b、c的值分别是-1、1、-1
### 6.运算顺序
#### 1> 算术表达式
用算术运算符将数据连接起来的式子,称为“算术表达式”。比如a + b、10 * 5等。如果表达式比较复杂的话,那么就要注意一下它的运算顺序。表达式的运算顺序是按照运算符的结合方向和优先级进行的。
#### 2> 结合方向
算术运算符的结合方向是从左到右。例如表达式2+3+4,先计算2+3。
#### 3> 优先级
优先级越高,就越先进行运算,当优先级相同时,参照结合方向。下面是算术运算符的优先级排序:
负值运算符(-) > 乘(*)、除(/)、模(%)运算符 > 加(+)、减(-)运算符
例如表达式4+5*8/-2的计算顺序为:-、*、/、+,最后的结果是-16
#### 4> 小括号
如果需要先计算优先级低的可以使用小括号()括住,小括号的优先级是最高的!
- 例如4+5*8-2默认的计算顺序是:*、+、-
- 如果想先执行加法运算,就可以这样写:(4+5)*8-2,最后的结果都是不一样的
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#labelTop)
##二、赋值运算符
赋值运算符又分两种:简单赋值运算符 和 复合赋值运算符。
### 1.简单赋值运算符 =
#### 1> 简单用法
其实这个等号 = 从讲[变量](http://www.cnblogs.com/mjios/archive/2013/05/07/3065522.html)开始就见过它了,它的作用是将右边的值赋值给左边。
~~~
1 int a = 10 + 5;
~~~
赋值运算符的结合方向是:从右到左,而且优先级比算术运算符低。因此先进行等号=右边的加法运算,运算完毕后再将结果赋值给等号右边的变量。最后变量a的值是15。
#### 2> 连续赋值
~~~
1 int a, b;
2
3 a = b = 10;
~~~
- 在第1行分别定义了int类型的变量a、b
- 第3行代码的意思:将10赋值给变量b,再把变量b的值赋值给a。所以最后变量a、b的值都是10
#### 3> 使用注意
等号=左边只能是变量,不能是常量!常量都是不可变的,怎么可以再次赋值呢?下面的写法是错误的:
~~~
1 10 = 10 + 5;
~~~
### 2.复合赋值运算符
- += 加赋值运算符。如a += 3+2,等价于 a = a +(3+2)
- -= 减赋值运算符。如a -= 3+2,等价于 a = a -(3+2)
- *= 乘赋值运算符。如a *= 3+2,等价于 a = a *(3+2)
- /= 除赋值运算符。如a /= 3+2,等价于 a = a /(3+2)
- %= 取余赋值运算符。如a %= 3+2,等价于 a = a %(3+2)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#labelTop)
##三、自增运算符和自减运算符
### 1.简介
- ++ 自增运算符。如a++,++a,都等价于a = a+1
- -- 自减运算符。如a--,--a,都等价于a = a-1
注意:你写个5++是错误的,因为5是常量。
### 2.++a和a++的区别
1> 单独使用++a和a++时,它们是没区别的
~~~
1 int a = 10;
2 a++;
~~~
~~~
1 int a = 10;
2 ++a;
~~~
上面两段代码的效果都是让a的值+1,最后a的值都为11
2> 下面这种情况,++a和a++就有区别了
~~~
1 int a = 10;
2
3 int b = ++a;
~~~
~~~
1 int a = 10;
2
3 int b = a++;
~~~
上面两段代码的执行结果是有区别的。
- 第1段代码:++a的意思是先对a执行+1操作,再将a的值赋值给b。因此最后a、b的值都是11
- 第2段代码:a++的意思是先将a的值拷贝出来一份,然后对a执行+1操作,于是a变成了11,但是拷贝出来的值还是10,a++运算完毕后,再将拷贝出来的值10赋值给了b,所以最后变量b的值是10,变量a的值是11
--a和a--的区别也是一样的。
3> 再来看一个比较刁钻的例子
~~~
1 int a = 10;
2
3 a = a++;
~~~
很多人一眼看上去,觉得最后a的值应该是11,其实最后a的值是10。前面已经说过a++的作用了,这里也是一样的。先将a的值拷贝出来一份,然后对a执行+1操作,于是a变成了11,但是拷贝出来的值还是10,a++运算完毕后,再将拷贝出来的值10赋值给了a,所以最后变量a的值是10
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#labelTop)
##四、sizeof
* sizeof可以用来计算一个变量或者一个常量、一种数据类型所占的内存字节数。
~~~
int size = sizeof(10);
printf("10所占的字节数:%d", size);
~~~
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766713bac.png)
,10是int类型的数据,在64bit编译器环境下,int类型需要占用4个字节
* sizeof一共有3种形式
- sizeof( 变量\常量 )
~~~
sizeof(10);
char c = 'a';
sizeof(c);
~~~
- sizeof 变量\常量
~~~
sizeof 10;
char c = 'a';
sizeof c;
~~~
- sizeof( 数据类型 )
~~~
sizeof(float);
~~~
注意,不可以写成sizeof float;
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#labelTop)
##五、逗号运算符
* 逗号运算符主要用于连接表达式,例如:
~~~
1 int a = 9;
2 int b = 10;
3
4 a = a+1 , b = 3*4;
~~~
* 用逗号运算符连接起来的表达式称为逗号表达式,它的一般形式为:
表达式1, 表达式2, … …, 表达式n
逗号表达式的运算过程是:从左到右的顺序,先计算表达式1,接着计算表达式2,...,最后计算表达式n
* 逗号运算符也是一种运算符,因此它也有运算结果。整个逗号表达式的值是最后一个表达式的值
~~~
1 int a = 2;
2 int b = 0;
3 int c;
4
5 c = (++a, a *= 2, b = a * 5);
6
7 printf("c = %d", c);
~~~
++a的结果为3,a *= 2的结果为6,b = a * 5的结果为30。因此,输出结果为:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7667317a8.png)
这里要注意的是,右边的表达式是有用括号()包住的,如果不用括号包住,也就是:
~~~
1 c = ++a, a *= 2, b = a * 5;
2 printf("c = %d", c);
~~~
输出结果将为:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7667430da.png)
,因为c = ++a也属于逗号表达式的一部分,跟后面的a *= 2以及b = a * 5是相互独立的
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#labelTop)
##六、关系运算符
### 1.“真”与“假”
1> 默认情况下,我们在程序中写的每一句正确代码都会被执行。但很多时候,我们想在某个条件成立的情况下才执行某一段代码。比如微信的这个界面:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76675626c.png)
如果用户点击了注册按钮,我们就执行“跳转到注册界面”的代码;如果用户点击了登录按钮,我们就执行“跳转到登录界面”的代码。如果用户没做出任何操作,就不执行前面所说的两段代码。像这种情况的话可以使用条件语句来完成,但是我们暂时不学习条件语句,先来看一些更基础的知识:如何判断一个条件成不成立。如果这个都不会判断,还执行什么代码。
2> 在C语言中,条件成立称为“真”,条件不成立称为“假”,因此,判断条件是否成立,就是判断条件的“真假”。那怎么判断真假呢?C语言规定,任何非0值都为“真”,只有0才为“假”。也就是说,108、-18、4.5、-10.5等都是“真”,0则是“假”。
### 2.关系运算符的简单使用
C语言中还提供了一些关系运算符,可以用来比较两个数值的大小。
- < 小于。比如a<5
- <= 小于等于。比如a<=5
- > 大于。比如a>5
- >= 大于等于。比如a>=5
- == 等于。比如a==5
- != 不等于。比如a!=5
关系运算符的运算结果只有2种:如果条件成立,结果就为1,也就是“真”;如果条件不成立,结果就为0,也就是“假”。
~~~
1 int a1 = 5 > 4; // 1
2
3 int a2 = 5 < 4; // 0
~~~
### 3.关系运算符的使用注意
1> 关系运算符中==、!=的优先级相等,<、<=、>、>=的优先级相等,且前者的优先级低于后者
例如2==3>1 :先算3>1,条件成立,结果为1。再计算2==1,条件不成立,结果为0。因此2==3>1的结果为0。
2> 关系运算符的结合方向为“从左往右”
例如4>3>2 :先算4>3,条件成立,结果为1。再与2比较,即1>2,条件不成立,结果为0。因此4>3>2的结果为0。
3> 关系运算符的优先级小于算术运算符
例如3+4>8-2 :先计算3+4,结果为7。再计算8-2,结果为6。最后计算7>6,条件成立,结果为1。因此3+4>8-2的结果为1。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#labelTop)
##七、逻辑运算符
有时候,我们需要在多个条件同时成立的时候才能执行某段代码,比如:用户只有同时输入了QQ和密码,才能执行登录代码,如果只输入了QQ或者只输入了密码,就不能执行登录代码。这种情况下,我们就要借助于C语言提供的逻辑运算符。
C语言提供了3个逻辑运算符:&&(逻辑与)、||(逻辑或)、!(逻辑非)。注意:这些都是英文字符,不要写成中文字符。跟关系运算符一样,逻辑运算的结果只有2个:“真”为1,“假”为0
### 1.&& 逻辑与
#### 1> 使用格式
“条件A && 条件B”
#### 2> 运算结果
只有当条件A和条件B都成立时,结果才为1,也就是“真”;其余情况的结果都为0,也就是“假”。因此,条件A或条件B只要有一个不成立,结果都为0,也就是“假”
#### 3> 运算过程
- 总是先判断条件A是否成立
- 如果条件A成立,接着再判断条件B是否成立:如果条件B成立,“条件A && 条件B”的结果就为1,即“真”,如果条件B不成立,结果就为0,即“假”
- 如果条件A不成立,就不会再去判断条件B是否成立:因为条件A已经不成立了,不管条件B如何,“条件A && 条件B”的结果肯定是0,也就是“假”
#### 4> 举例
逻辑与的结合方向是“自左至右”。比如表达式 (a>3) && (a<5)
- 若a的值是4:先判断a>3,成立;再判断a<5,也成立。因此结果为1
- 若a的值是2:先判断a>3,不成立,停止判断。因此结果为0
- 因此,如果a的值在(3, 5)这个范围内,结果就为1;否则,结果就为0
#### 5> 注意
- 若想判断a的值是否在(3, 5)范围内,千万不能写成33) && (a<5)
- C语言规定:任何非0值都为“真”,只有0才为“假”。因此逻辑与也适用于数值。比如 5 && 4的结果是1,为“真”;-6 && 0的结果是0,为“假”
### 2.|| 逻辑或
#### 1> 使用格式
“条件A || 条件B”
#### 2> 运算结果
当条件A或条件B只要有一个成立时(也包括条件A和条件B都成立),结果就为1,也就是“真”;只有当条件A和条件B都不成立时,结果才为0,也就是“假”。
#### 3> 运算过程
- 总是先判断条件A是否成立
- 如果条件A成立,就不会再去判断条件B是否成立:因为条件A已经成立了,不管条件B如何,“条件A || 条件B”的结果肯定是1,也就是“真”
- 如果条件A不成立,接着再判断条件B是否成立:如果条件B成立,“条件A || 条件B”的结果就为1,即“真”,如果条件B不成立,结果就为0,即“假”
#### 4> 举例
逻辑或的结合方向是“自左至右”。比如表达式 (a<3) || (a>5)
- 若a的值是4:先判断a<3,不成立;再判断a>5,也不成立。因此结果为0
- 若a的值是2:先判断a<3,成立,停止判断。因此结果为1
- 因此,如果a的值在(-∞, 3)或者(5, +∞)范围内,结果就为1;否则,结果就为0
#### 5> 注意
C语言规定:任何非0值都为“真”,只有0才为“假”。因此逻辑或也适用于数值。比如 5 || 4的结果是1,为“真”;-6 || 0的结果是1,为“真”;0 || 0的结果是0,为“假”
### 3.! 逻辑非
#### 1> 使用格式
“! 条件A”
#### 2> 运算结果
其实就是对条件A进行取反:若条件A成立,结果就为0,即“假”;若条件A不成立,结果就为1,即“真”。也就是说:真的变假,假的变真。
#### 3> 举例
逻辑非的结合方向是“自右至左”。比如表达式 ! (a>5)
- 若a的值是6:先判断a>5,成立,再取反之后的结果为0
- 若a的值是2:先判断a>3,不成立,再取反之后的结果为1
- 因此,如果a的值大于5,结果就为0;否则,结果就为1
#### 4> 注意
- 可以多次连续使用逻辑非运算符:!(4>2)结果为0,是“假”,!!(4>2)结果为1,是“真”,!!!(4>2)结果为0,是“假”
- C语言规定:任何非0值都为“真”,只有0才为“假”。因此,对非0值进行逻辑非!运算的结果都是0,对0值进行逻辑非!运算的结果为1。!5、!6.7、!-9的结果都为0,!0的结果为1
### 4.优先级
逻辑运算符的优先级顺序为: 小括号() > 负号 - > ! > 算术运算符 > 关系运算符 > && > ||
- 表达式!(3>5) || (2<4) && (6<1) :先计算 !(3>5)、(2<4)、(6<1),结果为1,式子变为1 || 1 && 0,再计算1 && 0,式子变为1 || 0,最后的结果为1
- 表达式3+2<5||6>3 等价于 ((3+2) < 5) || (6>3),结果为1
- 表达式4>3 && !-5>2 等价于 (4>3) && ((!(-5)) > 2) ,结果为0
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#labelTop)
##八、三目运算符
### 1.N目运算符
- 像逻辑非(!)、负号(-)这种只连接一个数据的符号,称为“单目运算符”,比如!5、-5。
- 像算术运算符、关系运算符、逻辑运算符这种连接二个数据的负号,称为“双目运算符”,比如6+7、8*5、5>6、4 && 0、
- 以此类推,连接3个数据的运算符,应该称为“三目运算符”
### 2.三目运算符
C语言提供了唯一一个三目运算符:条件运算符。
#### 1> 使用格式
表达式A ? 表达式B : 表达式C
#### 2> 运算结果
如果表达式A成立,也就是为“真”,条件运算符的结果就是表达式B的值,否则,就为表达式C的值
#### 3> 结合方向和优先级
- 优先级顺序为:算术运算符 > 关系运算符 > 条件运算符 > 赋值运算符
- 条件运算符的结合方向是“从右至左”
~~~
1 int a = 3>4 ? 4+5 : 5>4 ? 5+6 : 6>7+1;
~~~
上面的代码等价于
~~~
1 int a = (3>4) ? (4+5) : ( (5>4) ? (5+6) : (6>(7+1)) );
~~~
简化一下就是
~~~
1 int a = 0 ? 9 : ( 1 ? 11 : 0 );
~~~
继续简化为
~~~
1 int a = 0 ? 9 : 11;
~~~
所以a的值是11
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/07/3071334.html#labelTop)
##九、位运算符
所谓位运算就是对每一个二进制位进行运算。C语言一共提供了6种位运算符,只能对整数进行操作,分别是:&按位与、|按位或、^按位异或、<<左移、>>右移、~取反。
### 1.& 按位与
1> 使用形式:整数a & 整数b
2> 功能:整数a和b各对应的二进位相与。只有对应的两个二进位均为1时,结果位才为1,否则为0。参与运算的数以[补码](http://www.cnblogs.com/mjios/archive/2013/05/25/3068114.html)方式出现。
3> 举例:比如9&5,其实就是1001&101=1,因此9&5=1
4> 规律:
- 相同整数相&的结果是整数本身。比如5&5=5
- 多个整数相&的结果跟顺序无关。比如5&6&7=5&7&6
### 2.| 按位或
1> 使用形式:整数a | 整数b
2> 功能:整数a和b各对应的二进位相或。只要对应的二个二进位有一个为1时,结果位就为1,否则为0。参与运算的数以[补码](http://www.cnblogs.com/mjios/archive/2013/05/25/3068114.html)方式出现。
3> 举例:比如9|5,其实就是1001|101=1101,因此9|5=13
4> 规律:
- 相同整数相|的结果是整数本身。比如5|5=5
- 多个整数相|的结果跟顺序无关。比如5|6|7=5|7|6
### 3.^ 按位异或
1> 使用形式:整数a ^ 整数b
2> 功能:整数a和b各对应的二进位相异或。当对应的二进位相异(不相同)时,结果为1,否则为0。参与运算的数以[补码](http://www.cnblogs.com/mjios/archive/2013/05/25/3068114.html)方式出现。
3> 举例:比如9^5,其实就是1001^101=1100,因此9^5=12
4> 规律:
- 二进制中,与1相^就会取反,与0相^保持原位
- 相同整数相^的结果是0。比如5^5=0
- 多个整数相^的结果跟顺序无关。比如5^6^7=5^7^6
- 因此得出结论:a^b^a = b
### 4.~ 取反
1> ~为单目运算符,具有右结合性,使用形式:~整数a
2> 功能:对整数a的各二进位进行取反(0变1,1变0)
3> 举例:比如~9,其实就是~(0000 0000 0000 0000 0000 0000 0000 1001)=(1111 1111 1111 1111 1111 1111 1111 0110),因此~9=-10
### 5.<< 左移
1> <<是双目运算符,使用形式:整数a<<正数n
2> 功能:把整数a的各二进位全部左移n位,高位丢弃,低位补0。左移n位其实就是乘以2的n次方。
3> 举例:3<<4,3本来是0000 0011,左移4位后变成了0011 0000,因此3<<4 = 48 = 3 * 24
4> 需要注意的是:由于左移是丢弃最高位,0补最低位,所以符号位也会被丢弃,左移出来的结果值可能会改变正负性
### 6.>> 右移
1> >>是双目运算符,使用形式:整数a>>正数n
2> 功能:把整数a的各二进位全部右移n位,保持符号位不变。右移n位其实就是除以2的n次方。
3> 举例:32>>3,32本来是0010 0000,右移3位后变成了0000 0100,因此32>>3 = 4 = 32 / 23
';
C语言入门教程7-基本数据类型
最后更新于:2022-04-01 20:26:22
本文目录
- [一、取值范围](http://www.cnblogs.com/mjios/archive/2013/06/06/3069571.html#label0)
- [二、char](http://www.cnblogs.com/mjios/archive/2013/06/06/3069571.html#label1)
- [三、说明符](http://www.cnblogs.com/mjios/archive/2013/06/06/3069571.html#label2)
- [四、自动类型提升](http://www.cnblogs.com/mjios/archive/2013/06/06/3069571.html#label3)
- [五、强制类型转换](http://www.cnblogs.com/mjios/archive/2013/06/06/3069571.html#label4)
C语言有丰富的数据类型,因此它很适合用来编写数据库,如DB2、Oracle等大型数据库都是C语言写的。其中,提供了4种最常用的基本数据类型:char、int、float、double,使用这些数据类型,我们就可以定义相应的变量来存储数据。这讲就来深入研究一下基本数据类型的一些使用细节。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/06/3069571.html#labelTop)
##一、取值范围
我们已经知道,不同数据类型所占的存储空间是不一样的。比如在64bit编译器环境下,char类型占用1个字节,int类型占用4个字节。字节长度不一样,包含的二进制位数就不一样,能表示的数据范围也就不一样。因此,int类型能表示的数据范围肯定比char类型大。下面来简单算算64bit编译器环境下int类型的取值范围。
### 1.推算int类型的取值范围
int类型占用4个字节,所以一共32位,那么按理来说,取值范围应该是:0000 0000 0000 0000 0000 0000 0000 0000~1111 1111 1111 1111 1111 1111 1111 1111,也就是10进制的0 ~ 232 - 1。但是int类型是有正负之分的,包括了正数和负数,那怎么表示负数呢?就是拿最高位来当符号位,当最高位为0就是正数,最高位为1则是负数。即:1000 0000 1001 1011 1000 0000 1001 1011就是一个负数,0000 1001 0000 1101 0000 1001 0000 1101是一个正数。由于最高位是0才代表正数,因此最大的正数是0111 1111 1111 1111 1111 1111 1111 1111,也就是231 - 1。而最小的负数就是1000 0000 0000 0000 0000 0000 0000 0000,也就是-231(为什么是这个值呢?可以根据[前面章节](http://www.cnblogs.com/mjios/archive/2013/05/08/3068114.html)提到的负数的二进制形式,自己去换算一下,看看1000 0000 0000 0000 0000 0000 0000 0000是不是-231。算不出也不用去纠结,不影响写代码,知道有这么一回事就完了)。因此,int类型的取值范围是-231 ~ 231 - 1。
注意:这个推算过程是不用掌握的,大致知道过程就行了,而且这个结论也不用去记,大致知道范围就行了。
### 2.各种数据类型的取值范围
int类型的取值范围已经会算了,那么其他数据类型的取值范围就能够以此类推。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7663c1aae.png)
(注:float和double由于是小数,它们的存储方式是特别不一样的,所以它们取值范围的算法也很不一样,这里不做介绍,也不用去掌握。e38表示乘以10的38次方,e-38表示乘以10的负38次方。)
上面表格中列出的只是64bit编译器环境下的情况。如果你的编译器是16bit或者32bit,这些数据类型的取值范围肯定是不一样的。比如int类型,在16bit编译器环境下是占用2个字节的,共16bit,所以int类型的取值范围是:-215 ~ 215 - 1。
### 3.数值越界
#### 1> 例子演示
前面已经看到,每种数据类型都有自己的取值范围。如果给一个变量赋值了一个超出取值范围的数值,那后果会不堪设想。
~~~
#include
int main()
{
int c = 1024 * 1024 * 1024 * 4;
printf("%d\n", c);
return 0;
}
~~~
我们都知道,int类型能保存的最大值是231-1。在第5行给int类型的变量c赋值一个比231-1大的数值:232 (1024是210)
先看看在终端中的输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7663d605b.png)
,可以看出输出的值是0。
#### 2> 结果分析
我们可以简单分析一下为什么将232赋值给变量c之后输出的是0。232的 二进制形式是:1 0000 0000 0000 0000 0000 0000 0000 0000,一共有33位二进制数。变量c占用了4个字节,只能容纳32位二进制数,而且内存寻址是从大到小的,因此,变量c在内存中的存储形式是0000 0000 0000 0000 0000 0000 0000 0000,也就是0,最前面的那个1就不属于变量c的了。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7663e6be4.png)
#### 3> 结论
可以发现,如果超出了变量的取值范围,那么将损失精度,得到“垃圾数据”(“垃圾数据”就是指并非我们想要的数据)。可是,有时候我们确实要存储一个很大很大的整数,比231-1还大的整数,这该怎么办呢?这个就要用到类型说明符,这这讲的后面会讨论。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/06/3069571.html#labelTop)
##二、char
### 1.简单使用
char是C语言中比较灵活的一种数据类型,称为“字符型”。既然叫“字符型”,那肯定是用来存储字符的,因此我们可以将一个字符[常量](http://www.cnblogs.com/mjios/archive/2013/05/07/3065522.html)赋值给一个字符型变量。
~~~
#include
int main()
{
char c = 'A';
printf("%c\n", c);
return 0;
}
~~~
在第5行定义了一个char类型变量c,并将字符常量'A'赋值给了c。在第7行将字符变量c输出到屏幕,%c的意思是以字符的格式输出。
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76640a705.png)
### 2.字符常量一定要用单引号括住
1> 下面的写法是错误的:
~~~
int main()
{
char c = A;
return 0;
}
~~~
编译器会直接报第3行的错,错误原因是:标识符A找不到。你直接写个大写A,编译器会认为这个A是一个变量。因此写成'A'才是正确的,或者在第3行代码的前面再定义1个变量名叫做A的char类型变量。
2> 下面的写法也是错误的:
~~~
int main()
{
char c = "A";
return 0;
}
~~~
第3行中的"A"并不是字符常量,而是字符串常量,将字符串"A"赋值给字符变量c是错误的做法。字符串和字符的存储机制不一样,因此"A"和'A'是有本质区别的。
### 3.字符型变量还可以当做整型变量使用
1个字符型变量占用1个字节,共8位,因此取值范围是-27~27-1。在这个范围内,你完全可以将字符型变量当做整型变量来使用。
~~~
1 #include
2
3 int main()
4 {
5 char c1 = -10;
6
7 char c2 = 120;
8
9 printf("c1=%d c2=%d \n", c1, c2);
10 return 0;
11 }
~~~
由于第9行用的是%d,表示以十进制整数格式输出,输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76641d057.png)
。因此,如果使用的整数不是很大的话,可以使用char代替int,这样的话,更节省内存开销。
### 4.字符型变量只能存储单字节字符
其实字符有2种类型:单字节字符和双字节字符。
- 单字节字符:在内存中占用1个字节的字符。包括了26个英文字母的大小写、10个阿拉伯数字等字符;
- 双字节字符:在内存中占用2个字节的字符。包括了中国、日本和韩国等国家的文字,比如汉字。
1个字符型变量只占用1个字节,所以1个字符型变量只能存储1个单字节字符。
下面的写法是错误的:
~~~
1 #include
2
3 int main()
4 {
5 char c = 'ABCD';
6
7 printf("%c\n", c);
8 return 0;
9 }
~~~
编译器会对上面的代码发出警告,并不会报错,因此程序还是能够运行。由于变量c只能存储1个单字节字符,最终变量c只存储了'ABCD'中的'D'。
输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766433edc.png)
### 5.字符型变量不能存储汉字
在内存中,1个汉字需要用2个字节来存储,而1个字符型变量只占用1个字节的存储空间,所以字符型变量不能用来存储汉字。
下面的写法是错误的:
~~~
1 int main()
2 {
3 char c = '男';
4 return 0;
5 }
~~~
编译器会直接报第3行的错误。记住一个原则:单引号括住的必须是单字节字符。
### 6.ASCII
说到字符,就不得不提ASCII这个概念
1> ASCII是基于拉丁字母的一套电脑编码系统,是现今最通用的单字节编码系统,全称是“American Standard Code for Information Interchange”。编码系统,看起来好像很高级,其实就是一个字符集---字符的集合。
2> ASCII字符集包括了:所有的大写和小写英文字母,数字0到9,标点符号,以及一些特殊控制字符:如退格、删除、制表、回车,一共128个字符,全部都是“单字节字符”。
3> 在计算机中的任何数据都是以二进制形式存储的,因此每个ASCII字符在内存中是以二进制形式存储的,而且只占用1个字节,二进制数的值就称为这个ASCII字符的ASCII值。比如大写字母A在内存中的二进制形式是:0100 0001,那么它的ASCII值就是65。
4> 下面是一张ASCII码字符表,ASCII码值的范围是0~127
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766445624.png)
5> 我们都知道1个char型变量只占用1个字节的存储空间,而所有的ASCII字符都是单字节字符,因此char型变量能存储任何ASCII字符。而且在使用char型变量存储ASCII字符时,可以直接用ASCII字符,也可以用ASCII值。
~~~
1 #include
2
3 int main()
4 {
5 char c1 = 65;
6
7 char c2 = 'A';
8
9 printf("c1=%c c2=%c \n", c1, c2);
10 return 0;
11 }
~~~
在第5、7行分别定义了字符型变量c1、c2。很明显,变量c2存储的是ACII字符'A';变量c1存储的是65,而ASCII值65对应的ASCII字符就是'A',因此变量c1存储的也是'A'。
由于第9行用的是%c,表示以字符格式输出,输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76646750a.png)
5> 经过上面的例子后,应该知道6和'6'的区别了吧
~~~
1 #include
2
3 int main()
4 {
5 char c1 = 6;
6
7 char c2 = '6';
8
9 printf("c1=%d c2=%d \n", c1, c2);
10 return 0;
11 }
~~~
第5行给变量c1赋值了整数6,第7行给变量c2赋值了字符'6','6'的ASCII值是54。
由于第9行用的是%d,表示以十进制整数格式输出,输出结果:![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76648a1fd.png)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/06/3069571.html#labelTop)
##三、说明符
### 1.什么是说明符
1> 我们已经知道,在64bit编译器环境下,1个int类型变量取值范围是-231 ~ 231 - 1,最大值是231-1。有时候,我们要使用的整数可能比231-1还大,比如234这个整数,如果还坚持用int类型变量来存储这个值的话,就会损失精度,得到的是垃圾数据。为了解决这个问题,C语言允许我们给int类型的变量加一些说明符,某些说明符可以增大int类型变量的长度,这样的话,int类型变量能存储的数据范围就变大了。
2> C语言提供了以下4种说明符,4个都属于关键字:
- short 短型
- long 长型
- signed 有符号型
- unsigned 无符号型
按照用途进行分类,short和long是一类,signed和unsigned是一类。
### 2.用法演示
这些说明符一般就是用来修饰int类型的,所以在使用时可以省略int
~~~
1 // 下面两种写法是等价的
2 short int s1 = 1;
3 short s2 = 1;
4
5 // 下面两种写法是等价的
6 long int l1 = 2;
7 long l2 = 2;
8
9 // 可以连续使用2个long
10 long long ll = 10;
11
12 // 下面两种写法是等价的
13 signed int si1 = 3;
14 signed si2 = 3;
15
16 // 下面两种写法是等价的
17 unsigned int us1 = 4;
18 unsigned us2 = 4;
19
20 // 也可以同时使用2种修饰符
21 signed short int ss = 5;
22 unsigned long int ul = 5;
~~~
1> 第2行中的short int和第3行中的short是等价的。
2> 看第10行,可以连续使用两个long。long的作用会在后面解释。
3> 注意第21和22行,可以同时使用两种不同的说明符。但是不能同时使用相同类型的修饰符,也就是说不能同时使用short和long 或者 不能同时使用signed和unsigned。
### 3.short和long
1> short和long可以提供不同长度的整型数,也就是可以改变整型数的取值范围。在64bit编译器环境下,int占用4个字节(32bit),取值范围是-231~231-1;short占用2个字节(16bit),取值范围是-215~215-1;long占用8个字节(64bit),取值范围是-263~263-1
2> 总结一下:在64位编译器环境下,short占2个字节(16位),int占4个字节(32位),long占8个字节(64位)。因此,如果使用的整数不是很大的话,可以使用short代替int,这样的话,更节省内存开销。
3> 世界上的编译器林林总总,不同编译器环境下,int、short、long的取值范围和占用的长度又是不一样的。比如在16bit编译器环境下,long只占用4个字节。不过幸运的是,ANSI \ ISO制定了以下规则:
- short跟int至少为16位(2字节)
- long至少为32位(4字节)
- short的长度不能大于int,int的长度不能大于long
- char一定为为8位(1字节),毕竟char是我们编程能用的最小数据类型
4> 可以连续使用2个long,也就是long long。一般来说,long long的范围是不小于long的,比如在32bit编译器环境下,long long占用8个字节,long占用4个字节。不过在64bit编译器环境下,long long跟long是一样的,都占用8个字节。
5> 还有一点要明确的是:short int等价于short,long int等价于long,long long int等价于long long
### 4.long的使用注意
#### 1> 常量
long和int都能够存储整型常量,为了区分long和int,一般会在整型常量后面加个小写字母l,比如100l,表示long类型的常量。如果是long long类型呢,就加2个l,比如100ll。如果什么都不加,就是int类型的常量。因此,100是int类型的常量,100l是long类型的常量,100ll是long long类型的常量。
~~~
1 int main()
2 {
3 int a = 100;
4
5 long b = 100l;
6
7 long long c = 100ll;
8
9 return 0;
10 }
~~~
变量a、b、c最终存储的值其实都是100,只不过占用的字节不相同,变量a用4个字节来存储100,变量b、c则用8个字节来存储100。
其实,你直接将100赋值给long类型的变量也是没问题的,照样使用。因为100是个int类型的常量,只要有4个字节,就能存储它,而long类型的变量b有8个字节,那肯定可以装下100啦。
~~~
1 int main()
2 {
3 long b = 100;
4
5 return 0;
6 }
~~~
#### 2> 输出
~~~
1 #include
2
3 int main()
4 {
5 long a = 100000000000l;
6
7 printf("%d\n", a);
8 return 0;
9 }
~~~
在第5行定义了long类型变量a,在第7行尝试输出a的值。注意了,这里用的是%d,表示以十进制整数格式输出,%d会把a当做int类型来输出,它认为a是4个字节的。由于a是long类型的,占用8个字节,但是输出a的时候,只会取其中4个字节的内容进行输出,所以输出结果是:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76649aa66.png)
又是传说的垃圾数据
那怎样才能完整地输出long类型呢?应该用格式符%ld
~~~
1 #include
2
3 int main()
4 {
5 long a = 100000000000l;
6
7 printf("%ld\n", a);
8 return 0;
9 }
~~~
注意第7行,双引号里面的是%ld,表示输出1个long类型的整数,这时候的输出结果是:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7664ac2ad.png)
如果是long long类型,应该用%lld
~~~
1 #include
2
3 int main()
4 {
5 long long a = 100000000000ll;
6
7 printf("%lld\n", a);
8 return 0;
9 }
~~~
### 5.signed和unsigned
1> 首先要明确的:signed int等价于signed,unsigned int等价于unsigned
2> signed和unsigned的区别就是它们的最高位是否要当做符号位,并不会像short和long那样改变数据的长度,即所占的字节数。
- signed:表示有符号,也就是说最高位要当做符号位,所以包括正数、负数和0。其实int的最高位本来就是符号位,已经包括了正负数和0了,因此signed和int是一样的,signed等价于signed int,也等价于int。signed的取值范围是-231 ~ 231 - 1
- unsigned:表示无符号,也就是说最高位并不当做符号位,所以不包括负数。在64bit编译器环境下面,int占用4个字节(32bit),因此unsigned的取值范围是:0000 0000 0000 0000 0000 0000 0000 0000 ~ 1111 1111 1111 1111 1111 1111 1111 1111,也就是0 ~ 232 - 1
### 6.signed、unsigned也可以修饰char,long还可以修饰double
知道有这么一回事就行了。
~~~
1 unsigned char c1 = 10;
2 signed char c2 = -10;
3
4 long double d1 = 12.0;
~~~
### 7.不同数据类型所占用的存储空间
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7664d0db6.png)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/06/3069571.html#labelTop)
##四、自动类型提升
### 1.什么是自动类型提升
先来看看下面的一则运算
~~~
1 #include
2
3 int main()
4 {
5 int a = 10;
6
7 double d = a + 9.5;
8
9 printf("%f \n", d);
10
11 return 0;
12 }
~~~
1> 在第5行定义了一个int类型的变量a,赋值了一个整数10。
2> 接着在第7行取出a的值10,加上浮点数9.5,这里做了一个“加法运算”,并且将“和”赋值给d。所以d的值应该是19.5。
3> 在第9行使用格式符%f输出浮点型变量d,默认是保留6位小数的。输出结果为:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7664e48ef.png)
4> 看似这么简单的运算,其实包含了一些语法细节在里面。严格来说,相同数据类型的值才能进行运算(比如加法运算),而且运算结果依然是同一种数据类型。第7行的情况是:变量a的值10是int类型(4字节),9.5是double类型(8字节)。很明显,10和9.5并不是相同数据类型。按理来说,10和9.5是不允许进行加法运算的。但是,系统会自动对占用内存较少的类型做一个“自动类型提升”的操作,也就把10提升为double类型。也就是说,本来是用4个字节来存放10的,现在改为用8个字节来存放10。因此,10和9.5现在都是用8个字节来存放的,都是double类型,然后就可以进行运算了。并且把运算结果赋值给double类型的变量d。
5> 需要注意的是:经过第7行代码后,变量a一直都还是int类型的,并没有变成double类型。1个变量在它定义的时候是什么类型,那么就一直都是什么类型。“自动类型提升”只是在运算过程中进行的。
### 2.常见的自动类型提升
~~~
1 int main()
2 {
3 float a = 10 + 3.45f;// int 提升为 float
4
5 int b = 'A' + 32; // char 提升为 int
6
7 double c = 10.3f + 5.7; // float 提升为 double
8
9 return 0;
10 }
~~~
1> 注意第5行,系统会将字符'A'提升为int类型数据,也就是转为'A'的ASCII值后再跟32进行加法运算。'A'的ASCII值是65,因此变量b的值为65+32=97。
2> 这个自动类型提升,知道有这么一回事就行了,不用死记这规则,因为系统会自动执行这个操作。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/06/06/3069571.html#labelTop)
##五、强制类型转换
### 1.什么是强制类型转换
先来看看下面的代码
~~~
1 #include
2
3 int main()
4 {
5 int i = 10.7;
6
7 printf("%d \n", i);
8 return 0;
9 }
~~~
1> 注意第5行,我们将一个8个字节的浮点数10.7赋值给了只有4个字节存储空间的整型变量i。可以想象得到,把8个字节的内容塞给4个字节,肯定会损失精度。在第7行将变量i的值输出,输出结果是:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7665020e1.png)
输出值为10,这是必然的。
2> 这里面也有一点语法细节,其实第5行做了一个“强制类型转换”的操作:由于左边是int类型的变量i,那么就会强制把double类型的10.7转换为int类型的10,并且把转换后的值赋值给了整型变量i。由于C语言是语法限制不严格,所以系统会自动强制转换,如果换做是其他语法严格的语言,比如Java,第5行代码早就报错了。
3> 如果写得严格一点,明显地进行“强制类型转换”,应该这样写:
~~~
1 #include
2
3 int main()
4 {
5 int i = (int) 10.7;
6
7 printf("%d \n", i);
8 return 0;
9 }
~~~
注意第5行,在10.7的前面加了个(int),表示强制转换为int类型的数据。这样就绝对不会有语法问题了。总之你将一个浮点型数据转换为整型数据,就会丢失小数部分的值。
### 2.常见的强制类型转换
~~~
1 int main()
2 {
3 int a = 198l; // long 转换为 int
4
5 char b = 65; // int 转换为 char
6
7 int c = 19.5f; // float 转换为 int
8
9 return 0;
10 }
~~~
这个强制类型转换,知道有这么一回事就行了,不用死记这规则,因为很多时候系统会自动执行这个操作。
### 3.其他用法
前面看到的强制转换好像都是“大类型”转为“小类型”,其实这是不一样的,也可以由“小类型”转为“大类型”
~~~
1 int main()
2 {
3 int a = 10;
4
5 double b = (double)a + 9.6;
6
7 return 0;
8 }
~~~
注意第5行,先将a的值强制转换为double类型后,再跟9.6进行加法运算。这样的话,系统就不用执行“自动类型提升”的操作了。其实你不强转也可以的,因为系统会做一个“自动类型提升”的操作,将变量a的值10提升为double类型。知道有这用法就行了,以后某些地方会用得上。
';
C语言入门教程6-变量与内存
最后更新于:2022-04-01 20:26:19
本文目录
- [一、字节和地址](http://www.cnblogs.com/mjios/archive/2013/05/25/3068114.html#label0)
- [二、变量的存储](http://www.cnblogs.com/mjios/archive/2013/05/25/3068114.html#label1)
- [三、负数的二进制形式](http://www.cnblogs.com/mjios/archive/2013/05/25/3068114.html#label2)
- [四、变量的作用域](http://www.cnblogs.com/mjios/archive/2013/05/25/3068114.html#label3)
- [五、变量的初始化](http://www.cnblogs.com/mjios/archive/2013/05/25/3068114.html#label4)
在前面一节中简单介绍了[变量](http://www.cnblogs.com/mjios/archive/2013/05/07/3065522.html)的使用,当我们定义一个变量的时候,系统就会为变量分配一块存储空间。而变量的数值在内存中是以二进制的形式存储的,这讲来深入研究变量在内存中的一些存储细节。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/05/25/3068114.html#labelTop)
##一、字节和地址
为了更好地理解变量在内存中的存储细节,先来认识一下内存中的“字节”和“地址”。
1.计算机中的内存是以[字节](http://www.cnblogs.com/mjios/archive/2013/05/07/3065522.html)为单位的存储空间。内存的每一个字节都有一个唯一的编号,这个编号就称为地址。就好像酒店是以房间为单位的,每个房间都有一个唯一的房号,我们根据房号就能找到对应的房间。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76614a025.png)
里面的每个小框框就代表着内存中的一个字节,白色数字就是每个字节的地址(这里采取十六进制来显示,地址值是随便写的,仅作为参考,真实情况中的地址值不一定是这个),可以发现,内存中相邻字节的地址是连续的。
2.大家都知道,一个字节有8位,所能表示的数据范围是非常有限的,因此,范围较大的数据就要占用多个字节,也就是说,不同类型的数据所占用的字节数是不一样的。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/05/25/3068114.html#labelTop)
##二、变量的存储
### 1.变量类型的作用
跟其他编程语言一样,C语言中用[变量](http://www.cnblogs.com/mjios/archive/2013/05/07/3065522.html)来存储计算过程使用的值,任何变量都必须先定义类型再使用。为什么一定要先定义呢?因为变量的类型决定了变量占用的存储空间,所以定义变量类型,就是为了给该变量分配适当的存储空间,以便存放数据。比如char类型,它是用来存储一个字符的,一个字符的话只需要1个字节的存储空间, 因此系统就只会给char类型变量分配1个字节,没必要分配2个字节、3个字节乃至更多的存储空间。
### 2.变量占用多少存储空间
1> 一个变量所占用的存储空间,不仅跟变量类型有关,而且还跟编译器环境有关系。同一种类型的变量,在不同编译器环境下所占用的存储空间又是不一样的。我们都知道操作系统是有不同位数的,比如Win7有分32位、64位,编译器也是一样的,也有不同位数:16位、32位、64位(Mac系统下的clang编译器是64bit的)。由于我们是Mac系统下开发,就以64位编译器为标准。
2> 下面的表格描述了在64位编译器环境下,基本数据类型所占用的存储空间,了解这些细节,对以后学习指针和数组时是很有帮助的。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d766162835.png)
3> 下面的表格描述了在不同编译器环境下的存储空间占用情况
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76617a9f3.png)
### 3.变量示例
当定义一个变量时,系统就会为这个变量分配一定的存储空间。
~~~
int main()
{
char a = 'A';
int b = 10;
return 0;
}
~~~
1> 在64bit编译器环境下,系统为变量a、b分别分配1个字节、4个字节的存储单元。也就是说:
- 变量b中的10是用4个字节来存储的,4个字节共32位,因此变量b在内存中的存储形式应该是0000 0000 0000 0000 0000 0000 0000 1010。
- 变量a中的'A'是用1个字节来存储的,1个字节共8位,变量a在内存中的存储形式是0100 0001,至于为什么'A'的二进制是这样呢,后面再讨论。
2> 上述变量a、b在内存中的存储情况大致如下表所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d76619262b.png)
(注:"存储的内容"那一列的一个小格子就代表一个字节,"地址"那一列是指每个字节的地址)
- 从图中可以看出,变量b占用了内存地址从ffc1~ffc4的4个字节,变量a占用了内存地址为ffc5的1个字节。每个字节都有自己的地址,其实变量也有地址。变量存储单元的第一个字节的地址就是该变量的地址。变量a的地址是ffc5,变量b的地址是ffc1。
- 内存寻址是从大到小的,也就是说做什么事都会先从内存地址较大的字节开始,因此系统会优先分配地址值较大的字节给变量。由于是先定义变量a、后定义变量b,因此你会看到变量a的地址ffc5比变量b的地址ffc1大。
- 注意看表格中变量b存储的内容,变量b的二进制形式是:0000 0000 0000 0000 0000 0000 0000 1010。由于内存寻址是从大到小的,所以是从内存地址最大的字节开始存储数据,存放顺序是ffc4 -> ffc3 -> ffc2 -> ffc1,所以把前面的0000 0000都放在ffc2~ffc4中,最后面的八位0000 1010放在ffc1中。
### 4.查看变量的内存地址
在调试过程中,我们经常会采取打印的方式查看变量的地址
~~~
#include
int main()
{
int a = 10;
printf("变量a的地址是:%p", &a);
return 0;
}
~~~
第6行中的&是一个地址运算符,&a表示取得变量a的地址。格式符%p是专门用来输出地址的。输出结果是:
变量a的地址是:0x7fff5fbff8f8
这个0x7fff5fbff8f8就是变量a的内存地址
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/05/25/3068114.html#labelTop)
##三、负数的二进制形式
~~~
int main()
{
int b = -10;
return 0;
}
~~~
在第3行定义了一个整型变量,它的值是-10。-10在内存中怎样存储的呢?其实任何数值在内存中都是以补码的形式存储的。
- 正数的补码与原码相同。比如9的原码和补码都是1001
- 负数的补码等于它正数的原码取反后再+1。(取反的意思就是0变1、1变0)
那么-10的补码计算过程如下:
1> 先算出10的二进制形式:0000 0000 0000 0000 0000 0000 0000 1010
2> 对10的二进制进行取反:1111 1111 1111 1111 1111 1111 1111 0101
3> 对取反后的结果+1:1111 1111 1111 1111 1111 1111 1111 0110
因此,整数-10在内存中的二进制形式是:1111 1111 1111 1111 1111 1111 1111 0110
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-05-07_572d7661a5c5f.png)
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/05/25/3068114.html#labelTop)
##四、变量的作用域
### 1.作用域简介
变量的作用域就是指变量的作用范围。先来看看下面的程序:
~~~
int main()
{
int a = 7;
return 0;
}
~~~
- 在第3行定义了一个变量a,当执行到这行代码时,系统就会为变量a分配存储空间
- 当main函数执行完毕,也就是执行完第5行代码了,变量a所占用的内存就会被系统自动回收
- 因此,变量a的作用范围是从定义它的那行开始,一直到它所在的大括号{}结束,也就是第3~6行,一旦离开这个范围,变量a就失效了
### 2.代码块
1> 代码块其实就是用大括号{}括住的一块代码。
~~~
int main()
{
{
int a = 10;
printf("a=%d", a);
}
a = 9;
return 0;
}
~~~
- 注意第3~7行的大括号,这就是一个代码块
- 当执行到第4行时,系统会分配内存给变量a
- 当代码块执行完毕,也就是执行完第6行代码后,变量a所占用的内存就会被系统回收
- 因此,变量a的作用范围是从定义它的那行开始,一直到它所在的大括号{}结束,也就是第4~7行,离开这个范围,变量a就失效了
- 所以,上面的程序是编译失败的,第9行代码是错误的,变量a在第7行的时候已经失效了,不可能在第9行使用
2> 如果是下面这种情况
~~~
int main()
{
int a = 9;
{
int a = 10;
printf("a=%d", a);
}
return 0;
}
~~~
- 注意第3、6行,各自定义了一个变量a,这种情况是没问题的。C语言规定:在不同作用域中允许有同名变量,系统会为它们分配不同的存储空间。
- 在第3行定义的变量a的作用域是:第3~12行;在第6行定义的变量a的作用域是:第6~9行。
- 最后注意第8行:尝试输出变量a的值。那这里输出的是哪一个变量a呢?先看输出结果:
a=10
这里采取的是“就近原则”,也就是第8行访问的是在第6行定义的变量a,并不是在第3行的变量a。
[回到顶部](http://www.cnblogs.com/mjios/archive/2013/05/25/3068114.html#labelTop)
##五、变量的初始化
变量在没有进行初始化之前,不要拿来使用,因为它里面存储的是一些垃圾数据
~~~
#include
int main()
{
int c;
printf("%d", c);
return 0;
}
~~~
注意第5行的变量c,只是定义了变量,并没有给它赋初值。输出结果:
1606422622
可以发现,变量c里面存储的是一些乱七八糟的数据
';