内存相关概念
外存 :又称拓展存储器,长期存放数据,是可掉电的设备,常见的外存设备有:硬盘、flash、ROM、U
盘、光盘、磁带。
内存 :暂时存放数据的设备,掉电丢失数据,厂家的内存有:RAM,DDR.
内存分为物理内存 和虚拟内存。
物理内存:实实在在的存储设备;
虚拟内存:操作系统虚拟出来的内存。
操作系统会将虚拟内存和物理内存进行映射。
在 32 位操作系统下,每个进程的寻址范围位
0x00000000~0xffffffff
,即
4G,我们在编程时看到的内存地址都是虚拟地址。
在程序运行时,操作系统会将虚拟内存 进行分区:
堆:在动态申请内存时,会在堆区开辟空间;
栈:主要存放局部变量;
静态全局区:
位初始化的静态全局区:没有初始化的静态变量(static
修饰的变量),或全局变量存放在此区;
初始化了的静态全局区:初始化过的全局变量、静态变量存在此区。
代码区:存放代码的区;
文字常量区:存放常量的区;
指针的相关概念
本文均在 32 为平台上进行讨论。
系统给每个存储单元分配了一个编号,从
0x00000000~0xffffffff
,这个编号就是地址。
指针就是地址。
image
指针变量:一个存放地址编号的变量。
在 32 位平台下,地址总线是 32 位的,所以地址是 32 位编号,占用 4
个字节,所以在 32 位平台下指针变量占用 4 个字节。
鱼代表鱼,虾代表虾,乌龟代表是王八。对应指针变量只能存放对应类型变量的地址,例如
int
类型的指针只能存放 int
类型变量的地址。
char
占 1
个字节,它有一个地址编号,这个地址编号就是其地址;int
占 4
个字节,它占有 4 个字节的存储单元,有 4 个地址编号。
image
指针的定义方法
定义指针的语法:
1 2 3 数据类型 *指针变量名; int *p;int *p1, p2;
与指针相关的运算符:&
、*
.
&
是取地址运算符,*
是取值运算符。
image
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> int main (int argc, char *argv[]) { int a = 100 ; int *p; p = &a; printf ("a = %d %d\n" , a, *p); printf ("&a = %p %p\n" , &a, p); return 0 ; }
输出结果为:
image
指针大小:在 32 位系统下,所有类型的指针都是 4 个字节。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> int main (int argc, char *argv[]) { char *a; short *b; int *c; long *d; float *e; double *f; printf ("sizeof(a) = %lld\n" , sizeof (a)); printf ("sizeof(b) = %lld\n" , sizeof (b)); printf ("sizeof(c) = %lld\n" , sizeof (c)); printf ("sizeof(d) = %lld\n" , sizeof (d)); printf ("sizeof(e) = %lld\n" , sizeof (e)); printf ("sizeof(f) = %lld\n" , sizeof (f)); return 0 ; }
输出结果为:
image
指针的分类
char
型指针;
short int
型指针;
int
指针;
long
指针;
float
型指针;
double
型指针;
函数指针;
结构体指针;
指针的指针;
数组指针。
不管是声明指针,在 32 位操作系统上,均占有 4 个字节。
指针和变量的关系
指针可以存放变量的地址。
在程序中,访问变量的值可以字节使用变量名,例如:
也可以通过指针来访问变量的值:
1 2 3 int *p;p = &a; *p = 100 ;
注意:
指针在试用期一定要初始化。
指针只能指向开辟好空间的地址,不能随意保存地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> int main (int argc, char *argv[]) { int *p1, *p2, temp, a, b; p1 = &a; p2 = &b; printf ("请输入:a b的值:\n" ); scanf ("%d %d" , p1, p2); temp = *p1; *p1 = *p2; *p2 = temp; printf ("a=%d b=%d\n" , a, b); printf ("*p1=%d *p2=%d\n" , *p1, *p2); return 0 ; }
输出结果:
image
注意:对应类型的指针,只能保持对应类型数据的地址,如果想让不同类型的指针互相赋值,需要强制类型转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> int main (int argc, char *argv[]) { int a = 0x1234 , b = 0x5678 ; char *p1, *p2; printf ("%#x %#x\n" , a, b); p1 = (char *)&a; p2 = (char *)&b; printf ("%#x %#x\n" , *p1, *p2); p1++; p2++; printf ("%#x %#x\n" , *p1, *p2); return 0 ; }
输出结果:
image
注意:
使用 *
对指针取值时,取几个字节,由指针类型决定,int
类型的指针取
4 个字节,double
类型的指针取 8 个字节。
对指针进行 +1
操作时,指针会跳过若干字节,跳过的字节的大小有指针类型决定,例如
int
类型的指针会跳过 4 个字节。
指针和数组元素之间的关系
数组元素与指针的基本关系
变量存放在内存中,有自己的地址编号。数组时多个相同类型变量的集合,每个变量都占用内存空间,都有自己的内存编号,数组在内存中是连续存放的。
指针变量可以指向存放数组元素的地址。
1 2 3 4 5 6 7 8 9 10 #include <stdio.h> int main (int argc, char *argv[]) { int a[10 ]; int *p; p = &a[0 ]; return 0 ; }
访问数组数组元素的方法
方式 1: 数组名[索引]
方式 2: 指针名 + 下标
1 2 3 4 int a[10 ];int *p;p = a; p[2 ] = 100 ;
在 C 语言中,数组名就是数组的首地址,即第 0
个元素的地址,是个常量。
注意:p 和 a 不同,p 是指针变量,而 a 是个常量。所以可以使用等号给 p
赋值,但不能给 a 赋值。例如:int a[10]; a++;
这种用法是错误的,因为 a 是数组名,是一种地址常量。
方式 3: 通过指针运算加取值的方法来引用数组的元素
1 2 3 4 int a[10 ];int *p;p = a; *(p + 2 ) = 100 ;
p
是第 个元素的地址,p + 2
是
a[2]
这个元素的地址,对第二个元素的地址取值,即
a[2]
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdio.h> int main (int argc, char *argv[]) { int a[5 ] = {0 , 1 , 2 , 3 , 4 }; int *p = a; printf ("a[2]=%d\n" , a[2 ]); printf ("p[2]=%d\n" , p[2 ]); printf ("*(p+2) = %d\n" , *(p + 2 )); printf ("*(a+2) = %d\n" , *(a + 2 )); printf ("p=%p\n" , p); printf ("p+2=%p\n" , p + 2 ); printf ("&a[0] = %p\n" , &a[0 ]); printf ("&a[2] = %p\n" , &a[2 ]); return 0 ; }
输出结果:
image
指针的运算
指针可以加一个整数
往后指几个它指向的变量,结果还是个地址。
注意:一般来说,指针指向数组是加一个整数才有意义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> int main () { int a[10 ]; int *p, *q; p = a; q = p + 2 ; printf ("p = %p\n" , p); printf ("q = %p\n" , q); return 0 ; }
image
两个相同类型指针可以比较大小
注意:只有相同类型的指针指向同一个数组里的元素,比较大小才有意义。
指向前面元素的指针小于指向后面元素的指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> int main () { int a[10 ]; int *p, *q; p = &a[1 ]; q = &a[6 ]; if (p < q) { printf ("p < q\n" ); } else if (p > q) { printf ("p > q\n" ); } else { printf ("p = q\n" ); } return 0 ; }
输出结果:
image
两个相同类型的指针可以做减法
注意:只有相同类型的指针指向同一个数组里的元素,做减法才有意义。
做减法的结果是两个指针之间有多少个元素。
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> int main () { int a[10 ]; int *p, *q; p = &a[0 ]; q = &a[3 ]; printf ("%lld\n" , q - p); return 0 ; }
输出结果:
image
两个相同类型的指针可以相互赋值
注意:只有相同类型的指针才可以互相赋值(void *
类型的除外)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> int main () { int a = 100 ; int *p, *q; p = &a; printf ("a = %d %d\n" , a, *p); q = p; printf ("*q = %d\n" , *q); *q = 999 ; printf ("a = %d\n" , a); return 0 ; }
输出结果:
image
指针数组
指针可以保存数组元素的地址,也可以定义一个数组,数组中的元素是若干个相同类型的指针变量,这个数组就是指针数组 。
定义指针数组的语法:
例如:
1 2 3 4 5 6 int *p[10 ];int a;p[1 ] = &a; int b[10 ];p[2 ] = &b[3 ];
指针数组按照其元素进行分类。
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> int main () { char *name[5 ] = {"Follw me" , "BASIC" , "Greatwall" , "FORTRAN" , "Computer" }; int i; for (i = 0 ; i < 5 ; i++) { printf ("%s\n" , name[i]); } return 0 ; }
输出结果:
image
指针的指针
指针的指针又称为二级指针。
指针本身也是一个变量,也有地址,可以用二级指针保存其地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> int main (int argc, char *argv[]) { int a = 100 ; int *p = &a; int **q = &p; printf ("a = %d %d %d\n" , a, *p, **q); printf ("&a = %p %p %p\n" , &a, p, *q); printf ("&p = %p %p\n" , &p, q); printf ("&q = %p\n" , &q); return 0 ; }
输出结果:
image
字符串和指针
在 C 语言中,字符串是以 '\0'
结尾的若干字符的集合。
字符串的存储形式:数组、字符串指针、堆。
char string[100] = "I love C!";
, 定义了一个字符数组
string
,用来存放多个字符,并且使用 I love C!\0
给 string
数组初始化。
char *str = "I love C!"
,定义了一个指针变量
str
,这个字符串中的字符不能存放在 str
变量中,str
只存放了字符 I
的地址,"I love C!"
存放在文字常量区。
char *str = (char*)malloc(10 * sizeof(char));
,动态申请了
10 个字节的存储空间,使用首地址个给 str
赋值,可以使用
strcpy(str, "I love C")
的方式将字符串
"I love C!"
拷贝到 str
指向的内存中。
可修改性:
栈区和全局区内存中的内容是可修改的。
1 2 char str[100 ] = "I love C!" ;str[0 ] = 'y' ;
文字常量区的内容是不可修改的。
1 2 char *str = "I love C!" ;*str = 'y' ;
堆区的内容是可以修改的。
1 char *str = (char *)malloc (10 * sizeof (char ));
初始化:
字符数组、指向字符串的指针可以在定义时初始化:
1 2 char buf_aver[] = "hello world" ;char *buf_point = "hello world" ;
堆中存放的字符串不能初始化,只能使用
strcpy
、scanf
进行赋值:
1 2 3 4 char *buf_heap;buf_heap = (char *)malloc (15 ); strcpy (buf_heap, "hello world" );scanf ("%s" , buf_heap);
使用时赋值:
字符数组使用 scanf
或者 strcpy
进行赋值:
1 2 3 4 char buf_aver[128 ];buf_aver = "hello kitty" ; strcpy (buf_aver, "hello kitty" ); scanf ("%s" , buf_aver);
指向字符串的指针:
1 2 3 char *buf_point;buf_point = "hello kitty" ; strcpy (buf_point, "hello kitty" );
数组指针
二维数组
二维数组,有行,有列。二维数组可以看成有多个一维数组构成的,是多个一维数组的集合。
例如:
定义了一个 3 行 5 列的二维数组。
可以认为二维数组 a 由 3 个一维数组构成,每个元素是一个一维数组。
二维数组 a 中,a + 1
指向下一个元素,即下一个一维数组,即下一行。
1 2 3 4 5 6 7 8 #include <stdio.h> int main (int argc, char *argv[]) { int a[3 ][5 ]; printf ("a=%p\n" , a); printf ("a+1=%p\n" , a + 1 ); return 0 ; }
输出结果:
image
数组指针的概念
数组本身是一个指针,指向一个数组,对数组指针加
1,指向下一个数组。
数组指针可以用来保存二维数组的首地址。
数组指针的定义方法
数组指针的定义语法:
1 指向的数组的类型 (*指针名)[指向的数组元素的个数]
例如:
定义了一个指向有 5 个元素的 int
类型的指针
p,p + 1
会跳过一个有 5 个数组元素的数组。
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> int main (int argc, char *argv[]) { int a[3 ][5 ]; int (*p)[5 ]; printf ("a=%p\n" , a); printf ("a+1=%p\n" , a + 1 ); p = a; printf ("p=%p\n" , p); printf ("p+1=%p\n" , p + 1 ); return 0 ; }
输出结果:
image
数组指针的用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> void fun (int (*p)[5 ], int x, int y) { p[0 ][1 ] = 101 ; }int main () { int a[3 ][5 ] = {0 }; fun(a, 3 , 5 ); for (int i = 0 ; i < 3 ; i++) { for (int j = 0 ; j < 5 ; j++) { printf ("%d " , a[i][j]); } printf ("\n" ); } return 0 ; }
输出结果:
image
各种数组指针的定义
一维数组指针
一维数组指针,加 1 后指向下一个一维数组。
配合每行有 5 个 int
类型的二维数组来用:
1 2 3 4 5 6 7 8 9 int a[3 ][5 ];int b[4 ][5 ];int c[5 ][5 ];int d[6 ][5 ];p = a; p = b; p = c; p = d;
上述操作都是可以的。
二维数组指针
二维数组指针,加 1 后指向下一个二维数组。
配合三维数组来用,三维数组由若干 4 行 5 列的二维数组组成。
1 2 3 4 5 6 7 8 9 int a[3 ][4 ][5 ];int b[4 ][4 ][5 ];int c[5 ][4 ][5 ];int d[6 ][4 ][5 ];p = a; p = b; p = c; p = d;
上述操作都是可以的。
三维数组指针
三维数组指针,加 1 后指向下一个三维数组
p + 1
跳一个三维数组,配合
int a[7][4][5][6];
使用。
容易混淆的内容
指针数组: 是个数组,数组元素是指针。
数组指针: 是个指针,指向一个数组。
指针的指针: 是指向指针的指针。
数组名字取地址
对一维数组名取地址,变成一位数组指针,即加 1 跳一个一维数组。
a + 1
跳一个 int
元素,即 a[1]
的地址,a
和 a + 1
相差一个元素,4
个字节。
&a
就变成了一个一维数组指针,是
int(*p)[10]
类型的,(&a) + 1
和
&a
相差一个数组,10 个元素,40 个字节。
数组名字和指针变量的区别
1 2 3 int a[10 ];int *p;p = a;
相同点: a
是数组的名字,是
a[0]
的地址,p
也保存了 a[0]
的地址,即 a
和 p
都指向
a[0]
,所以在访问数组元素时,a
和
p
是等价的。
不同点:
a
是常量,p
是变量,可以使用
=
运算符为 p
赋值,但是不能为 a
赋值;
对 a
取地址和对 p
取地址得到的结果不同。对
a
取地址得到的是指针数组,对 p
取地址得到的是指针的指针。
多维数组中指针的转换
在二维数组中,行地址取 *
会将指针降级,由行地址变成指向这一行第 0 个元素的指针,取 *
之后还是会指向同一个地方,但指针类型不一样了。
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> int main () { int a[3 ][5 ]; printf ("a=%p\n" , a); printf ("a +1=%p\n" , a + 1 ); printf ("*a =%p\n" , *a); printf ("(*a)+1 =%p\n" , (*a) + 1 ); return 0 ; }
输出结果:
指针与函数的关系
指针作为函数的参数
指针可以作为函数的参数。
C 语言中的参数传递方式:复制传参、地址传参。
复制传参:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> void fun (int a, int b) { int temp; temp = a; a = b; b = temp; printf ("in fun: a = %d, b = %d\n" , a, b); printf ("&a = %p, &b = %p\n" , &a, &b); } int main (int argc, char *argv[]) { int a = 100 , b = 20 ; printf ("before fun: a = %d, b = %d\n" , a, b); printf ("&a = %p, &b = %p\n" , &a, &b); fun(a, b); printf ("after fun: a = %d, b = %d\n" , a, b); return 0 ; }
输出结果:
image
地址传参:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdio.h> void fun (int *p, int *q) { int temp; temp = *p; *p = *q; *q = temp; printf ("in fun: *p = %d, *q = %d\n" , *p, *q); printf ("p = %p, q = %p\n" , p, q); } int main (int argc, char *argv[]) { int a = 100 , b = 20 ; printf ("before fun: a = %d, b = %d\n" , a, b); printf ("&a = %p, &b = %p\n" , &a, &b); fun(&a, &b); printf ("after fun: a = %d, b = %d\n" , a, b); return 0 ; }
输出结果:
image
传数组
将数组为参数传递给函数,传递的是数组的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <stdio.h> void fun1 (int *p) { printf ("%d\n" , p[2 ]); printf ("%d\n" , *(p + 3 )); } void test1 () { int a[10 ] = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 }; fun1(a); } void fun2 (int (*p)[4 ]) { printf ("%d\n" , p[0 ][2 ]); printf ("%d\n" , *(*(p + 1 ) + 2 )); } void test2 () { int a[2 ][4 ] = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 }; fun2(a); } void fun3 (char **q) { int i; for (i = 0 ; i < 3 ; i++) { printf ("%s\n" , q[i]); } } void test3 () { char *p[3 ] = {"hello" , "world" , "kitty" }; fun3(p); } int main () { test1(); test2(); test3(); return 0 ; }
输出结果:
image
指针函数
返回值为指针的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> char *fun () { static char str[100 ] = "hello world" ; return str; } int main () { char *p; p = fun(); printf ("p = %s\n" , p); return 0 ; }
输出结果:
image
函数指针
在 C
语言中,函数名就是函数的首地址,可以定义一个函数指针变量指向这个函数。
函数指针的定义语法:
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> int max (int x, int y) {}int min (int x, int y) {}int main () { int (*p)(int , int ); p = max; p = min; return 0 ; }
调用函数的方法:
通过函数的名字调用:
1 2 3 4 5 6 7 8 9 #include <stdio.h> int max (int x, int y) {}int main () { int num; num = max(3 , 5 ); return 0 ; }
通过函数指针调用:
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> int max (int x, int y) {}int main () { int num; int (*p)(int , int ); p = max; num = p(3 , 5 ); return 0 ; }
函数指针数组:
是一个数组,每个元素都是一个函数指针。
定义方式:
1 返回值类型 (*指针名)[函数指针的个数](形参列表);
例如:
定义了一个函数指针数组,有 10 个元素,每个元素指向一个返回值为
int
,有两个 int
类型的参数的指针。
函数指针最常用的地方: 将函数作为另外一个是函数的参数传递过去,即回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <stdio.h> int add (int x, int y) { return x + y; }int sub (int x, int y) { return x - y; }int mux (int x, int y) { return x * y; }int dive (int x, int y) { return x / y; }int process (int (*p)(int , int ), int a, int b) { int ret; ret = (*p)(a, b); return ret; } int main (int argc, char *argv[]) { int num; num = process(add, 2 , 3 ); printf ("num = %d\n" , num); num = process(sub, 2 , 3 ); printf ("num = %d\n" , num); num = process(mux, 2 , 3 ); printf ("num = %d\n" , num); num = process(dive, 2 , 3 ); printf ("num = %d\n" , num); return 0 ; }
输出结果:
image
特殊指针
void
类型的指针: 万能指针,可以将任意类型的指针赋值给他,但
void
类型的指针要转换成其他类型的指针必须要强转。有些函数的参数或者返回值就是
void *
类型。
NULL
:空指针,编号为 0 的指针,地址为
0x00000000
,一般用 NULL
给指针初始化。
main 函数传参
C 语言中主函数的头为:
1 int main (int argc, char *argv[]) ;
argv
:记录输入的参数个数。
argv
:记录输入的参数值。
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> int main (int argc, char *argv[]) { int i; printf ("argc=%d\n" , argc); for (i = 0 ; i < argc; i++) { printf ("argv[%d]=%s\n" , i, argv[i]); } return 0 ; }
使用如下命令运行:
1 main10.exe nihao hello world
输出结果为:
image
可以看到第一个参数是程序在计算机上的绝对路径,从第 2
个参数开始才是我们输入的值。