int num=100;
计算机中数据都是存储在内存中,所以读写数据的本质实际上是读写内存,而目前读写内存的惟一方式就是经过变量名,这种方式被称为“直接访问”内存。数组
在计算机中,内存空间的最小单位为字节,操做系统会为每个字节内存空间编号,而且这个编号在当前程序中是惟一的。ide
假设图是宾馆中的一排房间,每一个房间中都住着一个球员, 例如:101 号房间住着 7号球员、105 号房间住着 2 号球员、113 号房间住着 1 号球员。函数
若是想要在这排房间中找到 2 号球员,只需知道他住 105 号房间便可。101 房号至关于内存地址、101 房间至关于内存空间、7 号球员至关于内存空间中的数据。工具
前面讲过,要想找到 7 号球员,必须经过房间号来查找。同理,在计算机中,要想读写内存空间中的数据,也能够先经过内存地址找到该内存空间,而后进行读写操做。测试
读写内存的 2 种方式:
第 1 种 经过变量名读写内存。
变量本质上是一块有名字的内存空间,经过变量名读写内存,如图所示:操作系统
第 2 种 经过内存地址读写内存。
在计算机内存中,每个字节内存空间都有一个编号,这个编号被称为内存地址。经过该地址能够读写对应的内存空间,如图所示:3d
在上一节中,讨论了内存空间与内存地址的关系,为了更加深刻了解这 2 者之间的关系,将使用 VS2012 自带的工具,来更加形象的分析。指针
测试代码以下:调试
#include<stdio.h> int main(void) { int num=999; printf("%p\n",&num); //输出变量 num 地址 getchar(); return 0; }
【注意】
第 5 行代码中,&(shift+7)是 C 语言中的取地址符。&num 表示计算变量 num 所对应内存空间的地址编号,也就是所谓的内存地址。%p 表示以 16 进制格式输出内存地址。
编写完上述程序后,下面就来经过工具一步步探索内存空间。
第 1 步 测试程序第 6 行,鼠标单击加断点:
第 2 步 运行程序,控制台会输出 16 进制的地址数据(每次运行可能都不同),
如图所示:orm
第 3 步 依次点击菜单【调试】->【窗口】->【内存】->【内存 1】,打开内存窗口,如图所示:
第 4 步 将控制台输出的数据,输入到【内存 1】窗口的【地址】栏中,而后按下回车键,如图所示:
第 5 步 在【内存 1】窗体内右键点击,而后选择【4 字节整数(4)】、【带符号显示】如图所示:
【说明】
由于 num 是 int 类型,32 位系统下占 4 字节,因此第 5 步选【4 字节整数】,其余类型数据依次类推。
第 6 步 查看【内存 1】窗口,能够看到整数+999,其实就是 999 只是显示了符号位+而已,如图所示:
分析:
一、 以上整个过程,首先第 4 行,经过变量名 num 将整数 999 写入内存空间中。
二、 第 5 行,使用&num 计算出变量 num 对应的内存空间地址 0019FE50。
三、 经过断点调试的方式,查看 0019FE50 地址空间中保存的数据 999。
四、 通过以上分析,变量 num 完整内存模型如图所示:
五、能够看到,访问一块内存空间能够经过变量名,也能够内存地址。
【调试技巧】
为了方便使用【内存 1】查看内存,建议将【列】选项设置为【自动】,如图所示:
前面介绍过,变量的本质是一块有名字的内存空间。其实这块内存空间不只有名字,并且有编号,在 32 位系统下,这个编号是一个 4 字节的整数。经过(&变量名)的方式能够获得这个整数,例如:
int num=10;
printf(“%p\n”,&num); //以 16 进制格式输出
这个编号与一块内存空间是一一对应的,经过这个编号能够找到对应内存空间。相似于现实生活中,知道某我的的家庭地址,就能够经过地址找他家同样。在 C 语言程序中将这个编号形象的称呼为“内存地址”,经过内存地址就能够找到对应的内存空间。
现阶段目前,在程序中获得内存地址的惟一方式就是:&变量名。因为这种方式获得的内存地址就是变量所对应的内存空间地址,又是经过变量名获得的,所以能够称为“变量地址”。这里务必清楚,变量地址本质上就是内存地址。
用于保存内存地址的变量,称为指针变量。在 C 语言程序中不只变量有类型,数据也是有类型的,例如:1(整数)、3.14(浮点数)、’c’(字符),须要使用与之匹配的类型变量进行保存。同理,内存地址也是一种数据,这种数据都是指针类型,所以须要指针类型变量来保存这种数据。
定义指针变量的通常形式为:
类型名 *变量名;
类型名表示该指针变量只能保存该类型变量的地址,*表示该变量是指针变量只能保存地址数据,变量名即该变量的名称。
例如:int *p_a;
int 表示该指针变量只能保存 int 类型变量的地址,*表示变量 p_a 是指针变量只能保存地址数据,p_a 即指针变量的名称。
指针变量和普通变量初始化方式相同,能够在变量定义时初始化,也能够先定义后初始化。例如:
int a=10; int* p_a=&a; //定义 int 指针变量 p_a,并将变量 a 地址赋值给 p_a 或者 int a=10; int*p_a; //先定义 int 指针变量 p_a p_a=&a; //而后将变量 a 地址赋值给变量 p_a
在 C 语言程序中,将某个变量的地址赋值给指针变量,就认为该指针变量指向了某个变量,例如:
int a=10;
int*p_a=&a;
上述程序中,将整数变量 a 的地址赋值给指针变量 p_a,就认为 p_a 指向了变量 a,如图所示:
能够看到,变量 a 中存储的是整数 10,而变量 p_a 中存储的是变量 a 的地址。有点像现实生活中的中介,想要访问数据 10,必须先找到指针变量 p_a,经过变量 p_a 中的数据&a,再找到变量 a,最后访问数据 10。
指针变量的引用分 2 种状况:
第 1 种 引用指针变量。
#include<stdio.h> int main(void) { int a=10; int b=20; int *p1,*p2; //定义指针变量 p一、p2 p1=&a; //p1 指向变量 a p2=p1; //p2 指向变量 a。p2 指向 p1 当前指向的变量 printf("&a=%p p1=%p p2=%p\n",&a,p1,p2); p1=&b; //p1 指向变量 b p2=p1; //p2 指向变量 b printf("&b=%p p1=%p p2=%p\n",&b,p1,p2); getchar(); return 0; }
运行结果如图所示(每次运行结果可能都不同):
第 2 种 引用指针变量指向的变量。
#include<stdio.h> int main(void) { int a=10; int* p=&a; //p 指向变量 a printf("a=%d *p=%d\n",a,*p); *p=20; //修改 p 指向变量 a 中的数据 printf("a=%d *p=%d\n",a,*p); getchar(); return 0; }
运行结果如图所示:
【易混】在定义变量的时候,*放到变量前,表名变量是指针类型;在使用变量的时候用来读写指针变量指向的值。
速记:
在 C 语言中,函数参数不只能够是字符型、整型、浮点型……等,还能够是指针类型,做用是将变量地址传递给函数形参。
主要目的就是:函数内部修改外部变量的值。
下面经过两个例子来讲明指针变量作函数参数的用法。
案例 1,函数内部改变外部变量的值
void test1(int* p) { printf("函数内读取指针指向的值:%d\n",*p); *p=*p+1; } int main(void) { int num=999; test1(&num); printf("函数外读取变量的值:%d\n",num); getchar(); return 0; }
案例 2: 封装函数,交换两个整型变量的值。
#include<stdio.h> void swap(int* p_a,int* p_b) { int temp=*p_a; *p_a=*p_b; *p_b=temp; } int main(void) { int a=10; int b=20; swap(&a,&b); printf("交换后 a=%d b=%d\n",a,b); getchar(); return 0; }
scanf 函数原型: int scanf(const char * _Format, ...) 头文件: #include<stdio.h> 参数列表: _Format :格式控制字符串,与 printf 函数中格式控制做用相同。 … :地址列表,由若干个地址组成。 功能: 获取用户按键输入的数据,并以指定格式写入到变量、或数组中。
#include <stdio.h> int main(void) { int a; scanf("%d",&a); //从键盘输入一个整数,写入变量 a 中 printf("%d",a); //输出变量 a 的值 getchar(); //接收使用 scanf 时按下的回车键 getchar(); //程序暂停,等待用户输入 return 0; } int main(void) { int i,j; printf("请输入第一个数:\n"); scanf("%d",&i); printf("请输入第二个数:\n"); scanf("%d",&j); printf("%d\n",i+j); getchar(); getchar(); return 0; }
scanf 还能够接收多个输入数据,例如:
#include <stdio.h> int main(void) { int a,b; scanf("%d %d",&a,&b); //获取输入数据,写入到变量 a、b printf("a=%d\n",a); //输出变量 a printf("b=%d",b); //输出变量 b getchar(); getchar(); return 0; }
输入:1 2 (1 和 2 之间以空格隔开),而后按下回车键,运行结果如图所示:
scanf 中数据类型必定不能用错,float 类型必须使用%f、double 类型必须用%lf、int 类型必须使用%d,若是使用错了就会发现结果很奇怪。
使用 scanf 须要注意的问题
(1)scanf 函数中应该传入变量地址,而不是变量名,例如:
int a,b;
scanf(“%d %d”,a,b);
这种写法是错误的,应该将“a,b”修改成“&a,&b”。
(2)从键盘获取多个数据时,相邻数据之间可使用空格、回车、tab 键做为两个数据之间的分隔符。例如:
int a,b;
scanf(“%d %d”,&a,&b);
第 1 种输入方式:
1 2 //1 和 2 之间以空格分隔
第 2 种输入方式:
1 2 //1 和 2 之间以 tab 键分隔
第 3 种输入方式:
1
2 //1 和 2 之间以回车分隔
(3)*若是在 scanf 函数中的格式控制字符串中除了占位符以外,还有其余字符,则在输入时也必须在对应的位置上输入相同的字符。例如:
int a,b,c;
scanf(“%d,%d,%d”,&a,&b,&c); //注意 scanf 中%d 之间以“,”分隔
输入:
1,2,3 (输入数据时,也必须以“,”分隔)
(4)使用 scanf 获取字符串时,只需传入字符数组名便可,取地址符&能够省略不写。例如:
char c[10];
scanf(“%s”,c); //能够省去&
输入:
hello
注意使用%s 的时候,字符串中不要有空格,不然行为很怪异。
scanf 有不少怪异的行为和坑,可是深刻的东西研究价值不大,所以只要会常规的用法便可。
数组本质上是一片连续的内存空间,每一个数组元素都对应一块独立的内存空间,它们都有相应的地址。所以,指针变量既然能够指向变量,也就能够指向数组元素。
在 C 语言中数组能够看做是相同类型变量的集合。通俗点讲,数组中每一个元素类型都是相同的。例如:
char ch[10] //数组 ch 能够看做是由 10 个 char 变量组成
int a[10] //数组 a 能够看做是由 10 个 int 变量组成
float f[10] //数组 f 能够看做是由 10 个 float 变量组成
数组本质上是一片连续的内存空间,数组元素又能够看做是单独的内存空间,数组就好像是一排房间,数组元素是单独的一个房间。所以,每一个数组元素也都有本身的内存空间地址,简称数组元素地址。
可使用指针变量来保存数组元素地址,例如:
int a[5]={1,2,3,4,5}; //定义长度为 5 的 int 数组
int* p_a; //定义指向 int 变量的指针变量 p_a
p_a=&a[0] //把 a 数组第 0 个元素地址赋给指针变量 p_a。至关于&(a[0])
p_a 中保存了数组 a 第 0 个元素地址,能够认为指针变量 p_a 指向数组 a 第 0 个元素,
如图所示:
由于数组元素本质上能够看做是单独的变量,因此引用指向数组元素的指针变量与引用指向变量的指针变量方式相同,直接使用*指针变量名便可。
#include<stdio.h> int main(void) { int i[5]={1,2,3,4,5}; int*p_i; p_i=&i[0]; printf("i[0]=%d *p_i=%d\n",i[0],*p_i); p_i=&i[1]; printf("i[1]=%d *p_i=%d\n",i[1],*p_i); getchar(); return 0; }
和访问变量方式相似,能够将经过数组名访问元素方式称为“直接访问”,将经过数组元素地址访问元素方式称为“间接访问”。
在计算机中内存的最小单位是字节,每一个字节都对应一个地址。若是一个变量占用多个字节,就会占用多个内存地址。例如:char 类型变量占 1 字节就对应 1 个地址、short 类型变量占 2 字节对应 2 个地址、int 类型变量占 4 字节对应 4 个地址…..其余类型依次类推。同理,数组元素类型不一样占用的内存地址也不一样。
#include<stdio.h> int main(void) { char c[5]; short s[5]; int i; for (i=0;i<5;i++) { printf("&c[%d]=%p ", i , &c[i]); printf("&s[%d]=%p \n", i ,&s[i]); } getchar(); return 0; }
运行结果如图所示:
在 C 语言中,数组名与数组首元素地址等价。
#include<stdio.h> int main(void) { int num[5]; printf("%p\n",num); //输出数组名 printf("%p\n",&num[0]); //输出数组首元素地址 getchar(); return 0; }
运行结果如图所示:
指针本质上就是内存地址,在 32 位操做系统下,内存地址只是 4 字节的整数。既然是整数,就能够进行加、减、乘、除…..等算术运算。不过须要注意,在 C 语言中通常只讨论指针加、减运算,乘、除等其余算术运算是没有意义。
在实际开发中,指针加、减多用于数组(或者连续内存空间)。当指针变量 p 指向数组元素时,p+1 表示指向下一个数组元素,p-1 表示指向上一个数组元素。注意加减运算都不是“移动一个字节”,而是移动一个“单元”,对于 int 来说一个单元是 4 个字节。
#include<stdio.h> int main(void) { int a[3]={1,2,3}; int* p=&a[0]; //p 指向 a[0] printf("%p %d\n",p,*p); //输出 p、p 指向的元素 p=p+1; //p 加 1 printf("%p %d\n",p,*p); //输出 p、p 指向的元素 getchar(); return 0; }
运行结果如图所示:
指针的加减法是指针和普通整数运算才有意义,两个指针加法没意义:p=p+n 表示 p 向下指 n 个单元,p=p-1 表示 p 向上指 n 个单元。
下面经过例子来了解一下指针减法。
#include<stdio.h> int main(void) { int a[3]={1,2,3}; int* p=&a[1]; //p 指向 a[1] printf("%p %d\n",p,*p); //输出 p、p 指向的元素 p=p-1; //p 减 1 printf("%p %d\n",p,*p); //输出 p、p 指向的元素 getchar(); return 0; }
运行结果如图所示:
当指针变量 p 指向数组元素时,p+一、p-1 分别表示指向下一个、上一个数组元素。依次类推,p+i、p-i 分别表示指向下 i 个元素,上 i 个元素。
p+i 不能超过数组最后一个元素,p-i 不能小于数组第一个元素。不然就会发生数组越界。
两个指针的加法没意义,两个指针的减法表示相差的单元的个数。
还常用到两个指针相减,例如:p2-p1。
当 p1 和 p2 都指向同一个数组中的元素时,p2-p1 才有意义。以数组 int a[5]为例:假设p2 指向元素 a[2],p1 指向元素 a[0],执行 p2-p1 时不是表示隔了多少个字节,而是表示 p2所指向的元素与 p1 所指向的元素之间隔了多少个元素。
下面经过例子来了解两个指针相减。
#include<stdio.h> int main(void) { int a[5]={1,2,3,4,5}; int* p1=&a[0]; //p1 指向元素 a[0] int* p2=&a[2]; //p2 指向元素 a[2] printf("p1=%p\n",p1); //输出 p1 printf("p2=%p\n",p2); //输出 p2 printf("%d\n",p2-p1); //输出 p2-p1 getchar(); return 0; }
运行结果如图所示:
总结:
函数参数不只能够是变量,也能够是数组,它的做用是将数组首元素地址传给函数形参。
【说明】
在 C 语言中,数组作函数参数时,是没有副本机制的,只能传递地址。也能够认为,数组作函数参数时,会退化为指针。
下面经过例子来了解数组作形参时,退化为指针。
#include<stdio.h> void getSize(int nums[5])//(int *nums) { int size=sizeof(nums); printf("size=%d\n",size); } int main(void) { int nums[5]={1,2,3,4,5}; int size=sizeof(nums); //计算数组 nums 总字节数 printf("size=%d\n",size); getSize(nums); getchar(); return 0; }
运行结果如图所示:
能够看到 2 次输出结果不同。这是由于当数组作函数形参时,会退化为指针。
void getSize(int nums[5])
退化为:
void getSize(int *nums)
在 32 位系统下,全部指针变量都占 4 个字节,所以第 5 行输出结果为 4。
因为数组作函数参数时,会退化为指针,致使没法在被调函数中计算传入的数组大小以及长度。为了解决这种问题,规定数组作函数参数时,必须传入数组长度,例如:
void getSize(int *nums,int length);
其中形参 length 表示数组 nums 的长度。
#include<stdio.h> void show(int *nums,int length) //定义函数show { int i; for (i=0;i<length;i++) //遍历数组 { printf("%d ",nums[i]); } } int main(void) { int nums[5]={1,2,3,4,5}; int length=sizeof(nums)/sizeof(int); //计算数组长度 show(nums,length); //调用show函数 getchar(); return 0; }
总结:
在 C 语言中,数组名等价于数组首元素地址。例如:int a[5],a 与&a[0]彻底等价。能够认为 a+i 等价于&a[i],a+i 指向 a[i],那么*(a+i)就是 a+i 所指向的数组元素 a[i]。所以,*(a+i)与 a[i]等价。
#include<stdio.h> int main(void) { int a[5]={1,2,3,4,5}; int i; int len=sizeof(a)/sizeof(int); for (i=0;i<len;i++) { printf("%d ",*(a+i)); } getchar(); return 0; }
不管是对于数组名来说仍是对于指针来说:*(a+i)与 a[i]等效
除了在声明数组时候 sizeof(数组名)和 sizeof(指针变量名)的不一样,其余时候“数组名”和“指针变量名”用法都是同样的。
在 C 语言中字符串本质上就是采用字符数组形式进行存储。前面介绍过,指针能够指向数值类型数组元素,也就能够指向字符类型的数组元素。本节将介绍指向字符数组元素的指针。
在 C 语言中,字符串存放在字符数组中,要想引用字符串有两种方式:
下面的代码很简单:
#include<stdio.h> int main(void) { char str[]="hello"; //定义字符数组 str printf("%s\n",str); //以%s 格式输出 str printf("%c",str[2]); //以%c 格式输出一个字符 getchar(); return 0; }
固然也能够这样使用:
#include<stdio.h> int main(void) { char *str="hello"; printf("%s\n",str); printf("%c",str[2]); //*(str+i) getchar(); return 0; }
两个 sizeof 同样吗?
char s1[]="hello"; char *s2="hello"; printf("%d,%d\n",sizeof(s1),sizeof(s2)); //6,4
【说明】
须要注意的是,不论是经过字符数组,仍是经过字符指针引用字符串。编译器都会自动在字符串末尾添加 0,下面经过内存工具,来查看字符串在内存中是如何存储的。
第 1 步 编写测试程序
#include<stdio.h> int main(void) { char *str="hello"; printf("%p\n",str); //输出字符串地址 getchar(); return 0; }
第 2 步 在 getchar 添加断点。
第 3 步 运行程序,记录 str 指向字符串地址。
第 4 步 在【内存 1】中输入字符串地址,而后按下回车键,如图所示:
【说明】
若是没有显示出如图的效果,能够参考如下步骤配置查看内存方式。
右键窗口任意位置,依次选择【1 字节整数】、【不带符号显示】、【ANSI 文本】。
能够看到字符串在内存中,是按照字符的 ASCII 码进行存储的,而且最后一位是 0 做为字符串结束标志。
(四) 字符串处理函数在 C 语言中字符串是很是重要的概念,字符串处理函数是针对字符串进行操做的一系列函数。主要包含在头文件中,本节将介绍经常使用的字符串处理函数。
str:string cpy:copy 函数原型: char *strcpy(char* dest, char *src); 头文件: #include<string.h> 参数列表: dest:目标字符数组。 src:源字符串。 功能: 把 src 指向的字符串复制到 dest 指向的字符数组中。 返回值: 返回 dest 指向的内存地址。 应用 strcpy 实现字符串复制
#include<stdio.h> #include <string.h> int main(void) { char *src="hello"; char dest[10]={0}; strcpy(dest,src); printf("%s\n",src); printf("%s\n",dest); getchar(); return 0; }
mem:memory,cpy:copy 函数原型: void *memcpy(void*dest, const void *src, int size); 头文件:#include<string.h> 参数列表: dest:目标地址空间 src: 源地址空间 size: 要复制的字节个数 功能: 从 src 指向的内存空间起始位置开始,拷贝 size 个字节到 dest 指向的内存空间中。 返回值: 返回 dest 指向的目标地址。
#include<stdio.h> #include <string.h> int main(void) { char* src="helloworld"; char dest[7]={0}; memcpy(dest,src,6); printf("%s",dest); getchar(); return 0; }
strcpy 和 memcpy 的区别:strcpy 是把源和目标都看过字符串类型,所以会碰到'\0'中止;而 memcpy 则会原样复制。
string compare 对于指针变量来说,==是比较是不是同一个地址。 函数原型: int strcmp(char*str1,char*str2); 头文件: #include 参数列表: str1:字符串 1 str2:字符串 2 功能: 从 str1 与 str2 的首字符开始逐个比较(比较字符的 ASCII 码),直到出现不一样的字符或遇 到’\0’为止。 返回值: (1) 字符串 str1 小于 str2,返回值为负数。 (2) 字符串 str1 等于 str2,返回值为 0。 (3) 字符串 str1 大于 str2,返回值为正数。
ANSI 标准规定,返回值为正数、负数、0 。而确切数值是依赖不一样的 C 实现的,好比有的平台就是返回 一、-一、0。
【说明】
字符串比较大小,不能使用算术运算符进行比较,例如:
str1>str二、str1==str二、str1<str2
使用算术运算符比较的是字符串首元素的地址,并非比较字符串的内容。
#include<stdio.h> #include<string.h> int main(void) { char *str1="hello"; char *str2="HELLO"; int result=strcmp(str1,str2); if (result>0) { printf("str1 大于 str2"); } else if(0==result) { printf("str1 等于 str2"); } else { printf("str1 小于 str2"); } getchar(); return 0; }
i:ignore case,忽略大小写。 stricmp:string ignore case compare 函数原型: int stricmp(char*str1,char*str2); 头文件: #include<string.h> strcmp 与 stricmp 用法基本相同,只是忽略了大小写进行比较而已。
#include<stdio.h> #include<string.h> int main(void) { char *str1="hello"; char *str2="Hello"; int result=stricmp(str1,str2); if (result>0) { printf("str1 大于 str2"); } else if(0==result) { printf("str1 等于 str2"); } else { printf("str1 小于 str2"); } getchar(); return 0; }