1_基本概念
1.0 导入
提示:以下内容尽可能地从初学者角度和理解出发,在更高的理解层次上不一定完全准确。
计算机
计算机的用处:计算。
不过遗憾的是,指导计算机进行计算是一件相当困难的事情。原因在于:计算机并不理解几乎任何知识。当你试着让它计算“1+7=”时,它并不理解什么是“1”“7”,更不可能理解“+”“=”这样更加难懂的概念。计算机是一个顶级的计算工具,但它不理解数学,或者其他的什么事情。计算机程序
计算机程序的作用:指导计算机进行计算。计算机会在能力范围内尽可能满足程序,直到程序终止。计算机可以执行机器语言程序,但C语言,以及Python、Java等高级编程语言是由人定义的,它们在计算机眼里是一堆乱码。编译,是一种将常用编程语言翻译为计算机能够执行的基础命令的过程。但我们暂且不需要理解编译过程,更不用理解究竟被翻译成了什么基础命令,程序设计者只需要使用编程语言就完全可以让计算机进行计算。
C语言
这个入门教程所讲述的编程语言,它是一种历史悠久的、通用的、过程式的编程语言,由丹尼斯·里奇(Dennis Ritchie)于1972年在AT&T的贝尔实验室为开发UNIX操作系统而创建。它是许多现代编程语言(如C++、C#、Java、Python等)的基石。
在绝大多数教程中,你见到的第一个例子大概是这样:
//此处为每一行进行了标号,在c中,空格、空白行是没有意义的。
#include <stdio.h>//1
int main(void){//2
printf("Hello, world!\n");//3
return 0;//4
}
如果编译并运行该程序,你会看到输出:
Hello, world!
很明显可以看出3行的printf("Hello, world!\n");
使得程序输出了一行内容Hello, world!
,剩下的部分在这个例子中没有产生明显作用。
下面从这个例子入手,我们可以试着开始理解一个C语言程序。
C语言的函数是C程序的基本模块,它们接受输入(参数),完成一些操作,然后返回一个输出(返回值)。它们可以接受很多参数作为输入,也可以完全不接受参数。函数可以有一个返回值,也可以没有返回值。
行1
#include <stdio.h>
导入了一个头文件stdio.h
。头文件是一些函数内容,其中包含了在第三行中用到的printf
函数。在此程序中导入此头文件的内容之后,编译器才可以理解printf
。stdio.h
是C编译器预定义的,包含了一些关于键盘、屏幕输入输出的内容。关于头文件的更多内容此教程不会涉及,只需要知道一些头文件定义了一些特定函数即可。要想使用这些预先定义好的函数,需要导入这些头文件。
行2
int main(void)
是一个函数头,这个函数包含了2、3、4行。int
表明函数的返回值(见第4行)是一个:整数。main
是函数的名字,每一个C程序会首先调用main
函数,因此不要给它起别的名字。(void)
是函数的形参列表,即main
函数接受的参数列表,void
的含义是:空。这个main
函数没有任何参数。并且很明显也不需要任何参数。
行3
printf("Hello, world!\n");
调用了一个名为printf
的函数,这个函数由上面的头文件stdio.h
给出定义。printf("Hello, world!\n");
调用函数printf
时给出的参数是"Hello, world!\n"
,printf
将会打印(显示在输出区域)一个字符串Hello, world!\n
,其中的\n
是换行符,作用与回车键类似,在这个例子中可有可无,但通常在输出多行时需要使用\n
换行。另外,这一行由一个;
结尾,分号;
代表一行的终止。
printf
函数的具体实现原理比较复杂,不过我们使用它时不需要关心过程,对于一个计算机程序而言,结果的正确性往往比其他的更重要。
行4
return 0;
是main
函数的返回语句,main
函数在这里终止。0
代表程序被正确执行,这个数字是一个整数int
,(通常)返回到操作系统中。对于初学者而言,第2行和第4行可以看做是一个固定的格式。
这样,我们就理解了这一个小小的C程序。
下面是一个含有更多内容的程序
#include <stdio.h>
int plus(int num1,int num2);
int main(void){
int a,b,c;
a=b=1;
c=plus(a,b);
printf("1+1=?\n");
int answer;
scanf("%d",&answer);
if(answer == c){
printf("Good job!\n");
}else{
printf("Wrong!\n1+1=%d",c);
}
return 0;
}
int plus(int num1,int num2){
return num1+num2;
}
这个小程序涵盖了以下的内容。本文档的原则是尽可能使用非常少的篇幅来讲述基本内容。
1.1 声明
int a,b,c;
像上面这样以数据类型(如int
、char
、float
、double
)开头的行是声明,这里是变量的声明。a,b,c是此处声明的三个变量,在声明之后它们将会出现在计算机内存中,其类型是int
。换句话说,声明创建了某个类型的变量,为它起名,并立即分配了内存位置。在这里我们只是声明了a,b,c,并没有为它们初始化一个值,因此它们所储存的值的不确定的(或者说“未定义的”)。如果你不希望这样,请在声明时指定一个值。如:int a = 1;
int plus(int num1,int num2);
这是一个函数声明,声明了一个函数plus
,返回值类型为int
,接受的参数类型是(int,int)
,即:两个参数,第一个是int
类型,第二个也是int
类型。num1,num2出现在这里其实并没有实际意义,它们会被编译器忽略。因此也可以是int plus(int,int);
(能正常编译但不建议使用),但通常使用num1,num2这样的名字来保证程序的可读性。
1.2 对表达式求值
程序执行过程中,计算机会对遇到的表达式进行求值。因此C表达式的一个重要特性是每一个表达式都对应一个值。表达式是由变量、常量、运算符以及函数调用等组成的,它依据运算符的运算规则进行计算,并返回一个值。
a=b=1;
这是一个表达式语句,其中包含了赋值运算符=
,变量a
,b
,常量1
。计算机根据运算符优先级和结合性进行求值,由于只有一种运算符=
,所以会对=
开始依照结合性进行求值。赋值运算符=
的结合型是:从右到左,因此会对右边的那个=
开始求值。=
会对右操作数求值(右值),将这个求得的值赋给左操作数的对象(左值),再返回这个右值。对于b=1
,对1
求右值得到:1,对b
求左值得到对象是一个变量,这个变量其实就是b。求值的“副作用”是1会被赋值给变量b,然后返回一个值1
。
这个被返回的值,参与了左边这个赋值运算符的求值过程,经过一个相类似的过程,a被赋值为1.同时此操作还是会返回一个值1
,但是这个值并没有被用到,因此被操作系统很快丢弃了。
以上过程等价于以下几种写法:
a = (b = 1);
//或者写成两个表达式语句,结果也是一样的
b = 1;
a = 1;
//你当然还可以这样
a=b=c=e=f=g=1;
//神经的用法,结果是a和b都会是1
a=b=114514!=114*514;
现在,我们来为其他的一些表达式求值。
c=plus(a,b);
在这一个表达式语句当中,赋值运算符会对右操作数求值,而这里的右操作数是一个函数。对函数求值会调用(见1.3)这个函数,并获得plus
函数的返回值作为求值结果。在这个例子中函数plus
的返回值2被赋给了变量c。
printf("1+1=?\n");
尽管没有运算符,但也可以看作是一个合法的表达式语句(在C语言中,赋值和函数调用都是表达式,并没有所谓的“赋值语句”和“函数调用语句”,这些语句其实都是表达式语句),并且也会对其求值,或者说调用了printf
函数用来打印结果。
1.3 函数
就如同上面已经提到过的那样,函数是C程序的基本组成部分。可以简单的将函数理解为一些被包装在一起的代码,拥有零个或几个接口, 一个或几个输出。函数应该在被调用前出现,可以是函数的声明或函数的定义。在上面的例子中,int plus(int num1,int num2);
是一个函数声明,而int plus(int num1,int num2){ return num1+num2; }
是函数的定义。每一个函数都必须要拥有一次定义,但声明是可选的。必须保证函数第一次被调用时已经被声明过(但在后面才定义)或者已经被定义过,函数的调用才是合法的。
程序运行时首先调用main
函数,然后由main
函数调用其他函数。
在这个例子中,c=plus(a,b);
这一行调用了plus
函数,并且向它传递了两个实际参数:a
的值和b
的值。需要注意的是,C语言中的一切函数调用都是传值调用,这意味着函数调用时会对括号内的表达式求值,再将值传递给函数的形式参数。函数无法修改a
的值和b
的值(但其实有其他办法做到这一点。将在后面介绍)。
plus
函数接受了main
函数传递的值,并且将这些值赋值给形式参数num1
和num2
。num1
和num2
是两个只存在于plus
函数中的变量,在plus
中发生的一切都是对外封闭的,并且函数调用结束后num1
和num2
会立即被销毁。plus
函数与main
函数在地位上没有什么区别,它也能进行各种前述的操作。在这个例子中它只有一行return num1+num2;
,即:为表达式num1+num2
求值,然后返回结果给调用者main
。因此c=plus(a,b);
相当于c=2;
。
1.4 使用scanf和printf输入和输出
printf
函数的用法是:打印第一个参数的字符串内容,如果后面还有参数,将会在对于后续的参数求值之后,插入到第一个参数的占位符中进行打印。
printf("Good job!\n");
只接受了一个参数"Good job!\n"
,所以直接打印。
printf("Wrong!\n1+1=%d",c);
接受了两个参数"Wrong!\n1+1=%d"
和c
,其中第二个参数c
的值2
将代替占位符%d
,因此输出是
Wrong!
1+1=2
%d
是适用于int
型整数的占位符。对于printf
函数而言必须使用合适的占位符来代替相对应的数据类型,如果类型失配,其结果是未定义的。常用的的占位符使用方式:
%d:用于输出有符号十进制整数。
%u:用于输出无符号十进制整数。
%f:用于输出浮点数(默认为六位小数)。
%c:用于输出单个字符。
%s:用于输出字符串。
printf
作为一个函数也有返回值(很少被用到),这个值是它打印的字符数量;如果出现打印错误则返回一个负值。
scanf
是C标准库中众多输入函数中的一种。它可以将标准输入(例如键盘)中的内容解释称不同的数据类型,并存储在变量中。类比于printf
,scanf
也有对应于特定数据类型的占位符。
%d:用于读取十进制整数,并将其存储在int类型的变量中。
%f:用于读取浮点数(默认为单精度浮点数,即float类型)
%c:用于读取单个字符,并将其存储在char类型的变量中。需要注意的是,%c不会跳过空白字符(如空格、制表符、换行符),如果需要在读取字符前跳过空白字符,可以在%c前加一个空格,即" %c"。
%s:用于读取字符串,直到遇到空白字符(空格、制表符、换行符)为止,并将其存储在字符数组(字符串)中。
在上面的例子中scanf("%d",&answer);
调用了scanf()
函数,第一个实参是"%d"
,意味着将接受的输入解释为十进制整数类型,并存储在第二个实参&answer
当中。这里需要注意的是&
运算符。&
将获取一个变量的内存地址。在scanf("%d",&answer);
中,scanf()
函数并不关心变量answer
的值,而是需要知道该变量的地址,这样才能将解释后的十进制整数写书到变量answer
中。因此应当这样使用scanf()
函数:
如果用scanf()读入一个基本变量的值,在变量名之前加上一个
&
运算符;
如果用scanf()读入的是一个字符数组,不使用&
。这是因为数组名在本质上储存的就是一个地址,准确来说是数组第一个变量的地址。
以下是一个使用示例:
#include <stdio.h>
int main(){
int age,height;
char name[20];
printf("enter your age and height.\n");
scanf("%d %d",&age,&height);//这里需要使用&
printf("enter your name.\n");
scanf("%s",name);//name是字符数组,name代表一个内存地址,不需要&
printf("alright,your name is %s,age is %d,height is %d.",name,age,height);
return 0;
}
1.5 if else分支
if else语句是最常用的分支跳转语句,它的通用形式非常简单:
if(expression)
statement1
else
statement2
expression
指“表达式”,而statement
是语句。
如果expression
为真(非0),则执行statement1
中的内容;
如果expression
为假(0),则执行statement2
中的内容;
其中,else
以及statement2
可以省略。 省略else
和statement2
之后,如果如果expression
为假(0),则不会执行任何内容。形式如下:
if(expression)
statement
在上面的示例中:
if(answer == c){
printf("Good job!\n");
}else{
printf("Wrong!\n1+1=%d",c);
}
answer == c
是一个表达式,其中==
是相等运算符(在C中=
是赋值运算符,因此只能使用==
来判断相等。如果在if else语句中不幸使用的是=
,那么往往会造成令人迷惑的错误),对于相等运算符==
,如果两端表达式的值相等则返回1
,否则,结果值为0
。
1
是非0数,因此被看做“真”,而0
被看做“假”。
关于循环、分支、跳转语句的详细内容将在第三章介绍。
2_变量与表达式
变量和常量是程序所面对的两种基本数据对象。
- 声明语句:说明变量的类型、名字,也可以初始化变量。
- 运算符:指定了变量、常量之间将要进行的操作。
- 表达式:通过组织变量、常量和运算符,得到新的值。
- 对象的类型决定了对象的取值范围(取值集合),对象可以执行的操作。
2.1 变量名
第一个字符必须是字母或下划线“_”。
变量名区分大小写。
通常:局部变量使用较短的变量名,外部变量使用更长更有具体含义的变量名。
2.2 数据类型
C语言只提供以下的数据类型
char a;//char字符,占用一个字节
int a;//int反应的是机器中最自然的整数类型
float a;//单精度的浮点型
double a;//双精度的浮点型
此外short和long限定符可以规定不同长度的类型。
short int sint;//(int可省略)
long int lint;
int
类型通常为16位或32位,short
类型通常为16位,long
通常为32位。
类型限定符signed
和unsigned
可用于限定有符号型与无符号型。例如,signed char
默认类型为-128~127,unsigned char
默认范围是0~255。
long double
表示高精度浮点数。
补充知识:
位(Bit)或比特
位是计算机存储和处理信息的最小单位。它只有两个可能的值:0 或 1,这代表了二进制系统中的两个基本状态。
- 字节(Byte)
字节(Byte)是计算机中常用的数据单位,它由8个位(bit)组成。因此,一个字节可以表示2^8(即256)个不同的值,范围从00000000(十进制中的0)到11111111(十进制中的255)。
字节是计算机存储和处理数据的基本单位之一,用于表示文本字符(如ASCII码中的字符)、数字等。- 更大的数据单位
除了位和字节之外,还有更大的数据单位,它们都是基于字节的倍数来定义的:
1 KB = 1024 字节
1 MB = 1024 KB
1 GB = 1024 MB
1 TB = 1024 GB- 32位与64位系统
32位系统:基于32位处理器的操作系统,其寻址空间和内部寄存器的长度都是32位。这意味着它一次可以处理32位(即4个字节)的数据。
64位系统:基于64位处理器的操作系统,其寻址空间和内部寄存器的长度都是64位。因此,它一次可以处理64位(即8个字节)的数据。
2.3 常量
-
整型 1200是
int
类型的常量,123456789L是long
类型的常量,其中的区别在于long
类型常量以l
或者是L
结尾。类似的,unsigned
常量以u
或U
结尾,unsigned long
以ul
或UL
结尾。
实际上,如果一个整数太大,即使没有后缀也会被当作long
类型处理。 -
浮点型 无后缀的浮点数为
double
类型。f
或F
表示float
类型,u
或U
表示unsigned float
类型。 -
整数可以被表示为十进制,八进制,十六进制。普通的数字1200当然是十进制,带有显式的前缀0即为八进制,前缀为
0x
或0X
即为十六进制。 -
一个字符常量其实也是一个机器意义上的整数。用单引号进行表示,如
'x'
表示字符x。单个字符在机器意义上的值就是它在机器字符集中的值(例如ASCII字符集)。尽管在许多情况下不需要完全了解,但字符常量的值(例如'0'
在ASCII字符集中是48)也是有实际用处的,它可以用于字符之间的比较,并且其实也可以参与数值运算。注意:'x'
与"x"
是不同的,区别在于单引号还是双引号的使用。前者是单个字符,后者是字符串(尽管它只有一个字符)。 -
常量表达式是一个仅仅包含了常量的表达式,它的值在编译时确定。例如
31+28
是一个常量表达式,它的值59
在编译时进行计算,并且可以出现在常量可能出现的任意位置。 -
字符串常量或者说字符串字面值是机器意义上的字符数组。字符串常量
"string"
存储在内存的某个位置,占用了7个物理存储单元,因为实际上还包含了隐式的'\0'
用来标志着字符串的结束。C语言对于字符串常量的长度没有限制,可以使用标准库函数strlen
来直接获取字符串的长度,但是'\0'
不会被计算进去。 -
最后介绍
enum
枚举类型。
与define
类似,枚举建立常量值与其含义之间的联系。enum days {Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday}; enum days today; today = Monday; if (today == Friday) { printf("It's Friday!\n"); } else { printf("It's not Friday.\n"); }
如果没有显式的说明,
enum
枚举型从0开始计数。第一个为0,第二个为1…以此类推。
也可以显式的赋值:enum color {RED = 1, GREEN, BLUE};
2.4 声明
所有变量在使用之前都必须声明。
可以在声明的同时就将变量进行初始化。
任何变量的声明都可以使用const
限定符进行修饰。const
限定符意味着变量的值不能够被修改,或是一个数组的全部元素都不能被修改。
2.5 类型转换
一般来说,自动类型转换当然是将适用范围比较窄的操作数转换为比较宽的操作数。
C语言并没有规定char
型变量是有符号数signed
还是无符号数unsigned
,而在将字符类型转换为整型时,此特性可能会导致结果出错。因此如果要在char
类型的变量中存储非字符的数据,最好显式声明signed
或unsigned
。
在任何表达式中都可以使用显式的强制类型转换。语法为(类型)表达式
,其准确的作用机理是,先对表达式求值,再对得到的值转换为目标类型,输出一个目标类型的新值。
例如:
sqrt((double)n);
假设n是int
型变量,则强制转换为double
型的值,再传递给函数sqrt
的形参。需要指出的是,n的值并没有改变,只是经过了对n求值的过程,并不存在对n的赋值。
此外,函数在被调用时,会根据函数声明中的参数类型,对给出的实参的值进行自动的强制类型转换。例如下面的情况
double sqrt(double);//sqrt函数声明
/*
*/
root2 = sqrt(2);//用2调用函数sqrt,此时将自动使用强制类型转换方法将int的2转换为double的2.0
2.6 自增运算符和自减运算符
++
和--
用于使变量自行加1或减1.
其特殊之处在于,++a
前缀使用时将先改变变量的值,之后再返回改变之后的变量;a++
后缀使用时先返回原有的变量,在此操作完成之后再改变变量的值。当然,在根本不需要使用变量的值,而只是想改变变量的时候,这两种用法没有区别。
++
和--
仅能作用于单个变量。(i+j)++
是非法的。
2.7 位操作符
C语言中提供了6个位操作符。
&
按位与|
按位或^
按位异或<<
左移>>
右移~
按位求反
&
屏蔽某些特定的位:
n = n & 0177;//将n中除了最低7位以外的二进制位都置于0。 (0177是16进制数,因为显式的前缀0)
|
将一些位强制置为1:
x = x | SET_ON;//根据常量SET_ON,将1中的对应位置为1
按位异或^
:位相同为0,不同为1。
移位运算符>>
和<<
用于左操作数二进制值的右移和左移,移动的位数由右操作数决定。表达式x<<2
表示x的二进制数左移2位,右边的空余位由0填补,因此也就等价于x*4
的值。
按位求反~
是一元运算符,将1变为0,0变为1。
2.8 条件表达式
下面这组语句用来求一个较大的数,将值保存在z中。
if(a>=b){
z=a;
}else{
z=b;
}
使用了三元运算符?:
的条件表达式提供了一种更加简洁的写法:
z = (a>=b) ? a : b ;
这种条件表达式的语法为exp1 ? exp2 : exp3 ;
其作用方法为:首先对exp1求值,若为真,将exp2的值作为表达式结果返回。若exp1为假则将exp3的值作为表达式结果返回。
无论如何,exp2和exp3中只有一个表达式会参与计算。
3_循环、分支、跳转
编程语言通常会至少提供3种形式的程序执行顺序(又称程序流):
- 按顺序执行语句
- 如果满足某一些条件,则重复执行一部分语句(循环)
- 通过测试条件,选择性地执行某些语句(分支)
本部分将介绍C语言中的三种循环语句for
,while
,do while
、三种跳转语句break
,continue
,goto
和两种常用的分支语句if else
,switch
。
3.1 循环:for
,while
,do while
for
是最常见的循环模式,其语法为:
for (初始化表达式; 条件表达式; 更新表达式) {
// 循环体
}
- 初始化表达式:在程序运行到
for
循环时立即执行,且只会执行一次。这是整个for
循环中最先执行的语句,通常用法是初始化某个循环变量。在此处声明的变量将只在for
循环内有效。 - 条件表达式:当即将执行循环体时,会判断条件表达式的真假。若为真(返回一个非0的值)则执行循环体。若为假(0)则直接结束
for
循环。 - 更新表达式:在循环体结束运行后立即执行更新表达式,之后才会判断条件表达式的真假。顾名思义,更新表达式通常都是更新了循环变量的值。
一个简单的示例:
#include <stdio.h>
int main(){
for(int i=1;i<=5;i++){
printf("now,i == %d \n",i);
}
return 0;
}
输出为
now,i == 1
now,i == 2
now,i == 3
now,i == 4
now,i == 5
for
循环将初始化、测试和更新组合在一起,更加灵活美观。
相比之下,while
循环则更加直观简洁:
while(expression){
//循环体
}
在程序执行到while
循环时,立即先判断expression
表达式的真假,若为真才执行循环体,若为假则while
循环立即结束。循环体执行完成后会再次判断expression
的真假,以此类推。
上面的for
循环写作while
循环:
#include <stdio.h>
int main(){
int i=1;
while(i<=5){
printf("now,i == %d \n",i);
++i;
}
return 0;
}
do while
循环会确保循环体至少执行1次。程序运行至do
时会直接执行循环体,循环体执行完毕后判断expression
的值,再决定是否再次执行循环体。
#include <stdio.h>
int main(){
int i=1;
do{
printf("now,i == %d \n",i);
++i;
}while(i<=5);
return 0;
}
在绝大多数情况下,这三类循环可以相互取代,只需要一些小小的修改。
3.2 跳转:break
,continue
和goto
break
,continue
常用于循环控制。
break;
是一个单独的语句,只能在循环体以及switch
语句中使用,其作用是立即结束此循环,程序将会在循环之后的语句继续执行。
continue
只在循环体中使用,会立即跳过这一轮的循环体。其作用等效于跳转到循环体的末尾,然后(视情况而定)执行下一次循环。
goto
语句可立即使得程序跳转至标签所在位置,并执行标签语句。原则上不需要在C程序中使用goto
。
a:printf("go to this statement.");//为printf语句增添了标签a
b: ;//为一个空语句增添了标签b
/*
......
*/
goto a;//在此次程序会跳转至printf
3.3 分支:if else
,switch
正如前面所介绍过的那样,简而言之,简单形式的if
语句可以让程序视情况而定是否要执行语句还是直接跳过语句,if else
则可以在两条语句当中做出选择,如果满足条件则执行第一条,不满足则执行第二条。
if else
可以嵌套。例如判断一个数字大于10小于20:
if(num > 10){
if(num<20){
printf("10<num<20");
}else{
printf("num>=20");
}
}else{
printf("num<=10");
}
switch
为分支程序流提供了更多选择。
switch(expression)
{
case 1:
statement 1;
break;
case 2:
statement 2;
break;
case 3:
statement 3;
break;
default:
statement 4;
break;
}
switch
首先对expression
求值,根据该表达式的结果,依次开始扫描标签case
,遇到相同的值时,跳转至此标签继续执行程序。这里使用了break;
语句来确保标签后语句执行后,程序会离开switch
,并继续执行switch
之后的语句。(否则后续的语句也会被依次执行!因此通常会在switch
中多次使用break;
)如果在所有case
中都没能找到符合的值,则会跳转至default
标签。default
标签是可以省略的。
4_指针与数组
前面三章的内容介绍了C语言一些基础的语言用法。从这一章开始引入指针(pointer)的概念,这也是C中最为重要的知识。但首先,我们需要从变量与地址的相关概念入手。
4.0 导入:变量与地址
让我们先从声明一个变量a
开始:
int a=1;
a
是一个整型变量,它位于计算机的内存当中的某个位置。我们不妨将计算机的内存看作是一条非常长的街道一侧的连成一排的房屋,每一栋房屋看做一个变量。它们占据的面积不同,都能够储存一个值,并且分别对应一个确定的地址。想象在这条街道上有一个房屋,它的名字是a
,地址为0x7fff5fbff8d0
,当我们访问它是得到一个值1
。
容易看出,
- 变量的名是由程序中的声明决定的,也是由程序编写者决定的,更易于使用和理解;
- 变量的地址通常是一串十六进制的数字,只有机器才能理解;
- 变量的值,是我们所需要写入、读取的。
需要注意的是,变量的名和变量的址之间的关联并不是由硬件所提供的,而是由编译器在编译过程为程序所实现的。变量的名只是给予程序编写者一个更加直观的方式去记住变量的址,而计算机硬件能且只能通过变量的十六进制内存地址去访问变量。
4.1 指针
在引入指针变量之前,先来看看&
和*
运算符。&
取址运算符提取了一个变量的内存地址,因此&a
的值就是变量a
的址。*
是解引用运算符,所谓的解引用是指*
作用于一个指针变量,根据其储存的地址,找到那个内存地址所对应的变量。
而所谓的指针,其实也就是一个储存了内存地址的变量。
int *p = &a;
这是指针p
的声明。需要注意它的声明格式int *p
代指变量p
是一个指向了int
型变量的指针。可以这样理解:声明指针变量p
,对其进行解引用*p
,得到的是一个int
型的数据。
&a
提取了变量a
的内存地址,并通过赋值运算符,赋值给指针p
。
所以,总结为:在这个声明中,我们声明了一个变量p
,它储存一个int
型变量的地址。在声明的同时进行了初始化,p
中储存的是a
的地址,或者也可以形象的说指针p
指向了整型变量a
。因此它等价于
int *p;
p = &a;
那么,指针有什么特别的作用呢?
*p = 2;
这个赋值表达式语句中,我们将2
赋值给了*p
。*p
是什么?很明显,*p
即为对p
执行解引用,得到的是p
指向的变量a
。因此也等价于:
a = 2;
这仅是关于指针的一个基础(且不必要)的使用方法。在以后的介绍中,会展示指针更强大的用法。
警告:未初始化和非法指针
需要注意的是以下这个非常常见的错误:
int *a;
*a = 1;
在这个例子当中,我们先是声明了一个一个名叫a的指针变量,紧接着就将值1
赋给了指针a
所指的内存位置。可是我们还没有为指针变量a
进行初始化,a
指向的内存位置是哪里呢?
答案是,没有办法预测a
指向的内存位置,对这个位置的内存低至赋值将引发内存错误。因此,所有的指针都必须在指向一个特定变量之后才进行解引用使用。此类疏忽大意也被称作“野指针”。
4.2 指针与函数参数
C的重要特性之一在于所有的函数实参与形参的传递都是值传递,这意味着函数形参的值变化都不会影响到实参值的变化。在C中,如果需要编写一个函数改变主函数的变量的值,则需要借助地址值,即所谓的“传址调用”。这是因为,变量的值会改变,而有一个变量的内存地址在其生命周期之中是不变的,而我们恰好可以利用变量地址的不变性,使用函数修改主调函数中变量的值。
swap(&a,&b);
/*
*/
void swap(int *pa,int *pb){
int temp = *pa;
*pa = *pb;
*pb = temp;
}
在这个例子中实参是两个int
,形参变量是两个指向int
的指针。记住,形参变量的声明周期仅仅局限于函数调用,但借助于指针的解引用间接访问了main
中变量的值,实现了两个变量a
、b
值的修改。
4.3 数组
考虑这个声明:int a[5];
声明了一个整型数组a[5]
,数组名是a
,它有五个int
类型的元素。这五个元素分别可以用a[0]
,a[1]
,a[2]
,a[3]
,a[4]
访问。注意这里的下标从0开始,一共5个元素,所以是a[0]
到a[4]
。
声明int a[5];
立即为这个数组预留了5个int
型的空间,但如果没有经过初始化,其在内存中的值是未定义的。
数组名a
其实也同时是一个指针常量,在数组预留空间是立即被定义。a
可以作为一个指针进行使用,a
所指向的位置正是内存中数组的起始位置,即a[0]
。
除优先级以外,对于数组成员的下标访问和解引用访问完全相同。
a[2]//array[subscript]
*(a + 2)//*(array + subscript)
使用下标进行数组访问时,子表达式subscript
首先进行求值,之后在指针常量的基础上产生偏移量,得到对应下标的数组元素。上述的两种不同访问方式在逻辑上是等同的,但落实在机器的具体实现上,通常使用下标访问的效率会略慢一些。
4.4 作为形参的数组
前面提到过作为一个指针常量的数组名。如果要在一个函数调用中使用数组,最直接的方法是直接传递这个指针常量的值,即就是数组名所代表的数组地址。
#include <stdio.h>
void plus(int * array,int num);
int main(){
int a[5] = {0,1,2,3,4};
int num = sizeof(a) / sizeof (a[0]);
plus(a,num);
printf("a[5] == {%d,%d,%d,%d,%d}",a[0],a[1],a[2],a[3],a[4]);
return 0;
}
/*
*/
void plus(int * array,int num){
for(int i = 0;i < num ; i++){
array[i]++;
}
}
在这个例子中,无返回值函数plus
接受了a
作为int *
类型的形参,并通过下标访问来改变了数组中元素的值。C语言中的函数调用都是传值调用,但是此处只传递了一个地址,之后函数根据这个地址(“指针”)来访问内存中存在的变量。因此,这个函数plus
并不需要返回值,也一样能够正确完成任务。
5_结构与联合
数组提供了一种储存多个同类型变量的方法。如果我们想要在一起储存多个不同类型的变量,例如一个人的姓名,身高,年龄,就需要用到结构将不同类型的值存储在一起。
结构是一种由多个变量捆绑在一起的数据类型,由程序编写者定义其内容,类似于数组进行使用。
5.1 结构声明
结构的关键字是struct
,其标准声明语法是:
struct tag {member-list} variable-list;
下面声明一个包含3个不同类型成员的结构x:
struct my_struct{
int a;
char b;
float c;
}x;
仔细观察这个例子与标准语法的对应关系。
标签字段(tag
)将my_struct
作为这个成员列表的类型名,这样它可以代表后续的成员列表(member-list
)。而x
是这个特定的结构体的名字。
你可以这样使用该结构my_struct
进行更多结构体的创建。
struct my_struct y;
这样就创建了另一个my_struct
类型的结构体y
,它与x
具有相同的类型。
在标准声明语法中,可以省略variable-list
或tag
其中之一。
struct my_struct{
int a;
char b;
float c;
} ;
定义了my_struct
的成员构造,但没有实例化成某个特定的结构体。如果不想立即创建实例化的结构体,这种声明比较方便。
struct {
int a;
char b;
float c;
}x;
定义了一个结构体x
,但没有为成员变量指定一个标签。
struct my_struct;
一个不完整的结构声明:但它是合法的。
5.2 关于结构的操作
- 访问结构成员
访问一个结构体中的变量需要使用点运算符.
。
点运算符.
是二元运算符,接受两个变量:左操作数是结构体的名字,右操作数是结构体中特定变量的名字。
x.a = 1;
为结构体x
中的变量a
(int
类型)赋值为1
。
使用指针间接访问结构成员时,可以使用简写的p->a
代替(*p).a
,两者是等价的。 &
取址
#include <stdio.h>
int main(){
struct my_struct{
int a;
char b;
float c;
}x;
x.a = 1;
struct my_struct * p = &x;
printf("x.a == %d \n",(*p).a);
return 0;
}
//在例子中,p是指向my_struct类型结构的指针。此处它指向x。
- 作为一个整体进行赋值与复制
可以直接复制一个已有的结构:struct my_struct y = x;
当需要在函数中使用结构时,可以传递整个结构(传值调用),或者传递结构指针(传址调用)。
传递整个结构:void func(struct my_struct x)
复制了原结构。
传递结构指针:void func(struct my_struct *p)
传递了原结构的地址到一个结构指针。
5.3 联合
联合union
是一种可以在不同的时刻保存不同类型值的变量。
联合的语法和结构高度相似,唯一不同在于其特殊的存储逻辑:成员之间共享位置,相互覆盖。例如:
union u_tag{
int ival;
float fval;
char cval;
}u;
联合u
在一处存储值,可以是int
、float
或char
类型的变量,但一次只能存在其中的一个。
编译器能够确保u
占用足够大的内存以存放其中某一个特定变量。u
中的三种变量,任何一种类型都可以赋值给u
的成员,并且在随后的表达式中出现。需要注意的是,读取的数据类型必须与最近一次写入的数据类型相同,否则结果是未定义的。
联合名.成员名
或指向联合的指针->成员名
都可以用来访问结构成员。