5.指针
5.1 关于内存那点事
存储器:外存和内存
外存:长期存放数据,掉电不会丢失数据,如硬盘、光盘、ROM等
内存:暂时存放数据,掉电数据丢失,如RAM,DDR等
-
内存:物理内存和虚拟内存
物理内存:实实在在的存储设备
虚拟内存:操作系统虚拟出来的内存
操作系统会在物理内存和虚拟内存之间做映射,在32位系统下,每个进程的寻址范围是4G,
0x00000000 ~ 0xffffffff
在写应用程序的时候,能够看到的都是虚拟内存。
在32位系统中,虚拟内存被分为两个部分:3G的用户空间 + 1G的内核空间。
其中,用户空间是当前进程私有的,而内核空间是所有进程共有的。
-
在运行程序的时候,操作系统会将虚拟内存进行分区
堆:在动态申请内存的时候,在堆里开辟空间
栈:主要存放局部变量
静态全局区:
-
未初始化的静态全局区:
静态变量(定义变量的时候,加上static修饰)
全局变量,没有初始化的,存储在该区
-
初始化的静态全局区
全局变量、静态变量,赋初始值的存放在该区
代码区:存放代码
文字常量区:存放常量
-
内存以字节为单位来存储数据,可以将程序中的虚拟寻址空间看成很大的一维字符数组
5.2 指针的概念
操作系统给每个存储单元分配编号,从`0x00000000~0xffffffff`
指针变量:用来存放地址编号,在32的平台下,地址总线是32位的,所以地址编号为32位,即4个字节
注意:
- 无论什么类型的地址,都是存储单元的编号,在32位平台下即为4个字节
- 对应类型的指针变量只能存放对应类型的变量地址,如整形指针变量只能存放整形变量的地址
扩展:
字符变量`char ch;`ch占用1个字节,它有一个地址编号,这个地址编号就是ch的地址
整型变量`int a;`,`a`占用4个字节,它占用4个字节的存储单元,有4个地址编号
5.3 指针的定义
-
简单的指针
数据类型
*
,指针变量名int *p;
在定义指针的时候,
*
用来修饰变量,说明p这个变量是指针变量 -
指针运算符
&
取地址符,获取变量的地址*
取值#include <stdio.h> int main() { int a = 100; // 定义指针变量 int *p; p = &a; printf("a的地址为%p\n", p); printf("a = %d", *p); return 0; }
输出结果:
a的地址为00000052a75ff794 a = 100
扩展:
如果一行中定义多个指针变量,每个指针变量前面都需要加上
*
x修饰int *p,*q;
#include <stdio.h> int main(){ char *a; short *b; int *c; long *d; float *e; double *f; printf("sizeof(a) = %d\n",sizeof(a)); printf("sizeof(b) = %d\n",sizeof(b)); printf("sizeof(c) = %d\n",sizeof(c)); printf("sizeof(d) = %d\n",sizeof(d)); printf("sizeof(e) = %d\n",sizeof(e)); printf("sizeof(f) = %d\n",sizeof(f)); }
输出结果
sizeof(a) = 8 sizeof(b) = 8 sizeof(c) = 8 sizeof(d) = 8 sizeof(e) = 8
5.4 指针的分类
按指针的指向的数据类型分类:
-
字符指针
char *p; // 定义一个字符型指针,用来存放字符型变量的地址 char ch; p = &ch;
-
短整型指针
short int *p; // 定义一个短整型指针,用来存放短整型变量的地址 short int a; p = &a
-
整型指针
int *p; // 定义一个整型指针,用来存放整型变量的地址 int a; p =&a;
-
长整型指针
long int *p; // 定义一个长整型指针,用来存放长整型变量的地址 long int a; p =&a;
-
float指针
float *p; // 定义一个浮点型指针,用来存放浮点型变量的地址 float a; p =&a;
-
double指针
double *p; // 定义一个浮点型指针,用来存放浮点型变量的地址 double a; p =&a;
-
函数指针
-
结构体指针
-
指针的指针
5.5 指针和变量的关系
**指针可以存放变量的地址编号**
在程序中,引用变量的方法:
-
直接通过变量的名称
int a; a = 100;
-
通过指针变量来引用
int *p; int a = 100; p = &a; //*p 这个时候就是100
#include <stdio.h> int main() { int a = 100, b = 200; int *p, *q; p = &a; q = &b; printf("*p = %d,*q = %d\n", *p, *q); }
输出结果:
*p = 100,*q = 200
注意:
-
指针变量在定义的时候可以初始化
int a; int *p = &a; // 指针变量就是来存放地址的,*指针变量 就相当于是指向变量,如*p就是指向a;
-
指针只能保存开辟好的地址空间,不能随意保存地址
-
*指针
取值,取几个字节由指针类型决定:字符取一个字节,整型取4个字节,double取8个字节 -
指针++
指向下一个字符数据,指针存放地址的编号 +1;如果是整形数据,指针++
,指针存放的地址编号+4;
#include <stdio.h> int main() { int a = 0x1234,b = 0x5678; char *p1,*p2; printf("a = %#x b = %#x\n",a,b); p1 = (char *) &a; p2 = (char *) &b; printf("*p1 = %#x *p2 = %#x\n",*p1,*p2); p1++; p2++; printf("*p1 = %#x *p2 = %#x\n",*p1,*p2); return 0; }
输出结果
a = 0x1234 b = 0x5678 *p1 = 0x34 *p2 = 0x78 *p1 = 0x12 *p2 = 0x56
-
5.6 指针与数组元素的关系
-
指针变量存放数组元素地址
int a[10]; int *p; p = &a[0]; // 指针变量p保存了数组a中第0个元素的地址,即a[0]的地址
-
数组元素的引用方法
// 数组名[下标] int a[0]; a[2] =100; // 指针[下标] int a[10]; int *p; p = a; //p此时和a等价,数组的名字就是数组的首地址,是一个常量; // 注意:p是变量,a是常量,可以用常量给变量赋值 // 通过指针运算加取值的方法来引用数组元素 int a[10]; int *p; p=a; *(p + 2 ) = 100; // 相当于a[2] = 100
#include <stdio.h> int main() { int a[5] = {1, 2, 3, 4, 5}; int *p; 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+2) = %p\n", p); printf("&(p+2) = %p\n", p + 2); printf("&a[0] = %p\n", &a[0]); printf("&a[2] = %p\n", &a[2]); }
输出结果
a[2] = 3 p[2] = 3 *(p+2) = 3 *(a+2) = 3 &(p+2) = 0000000fdc7ff680 &(p+2) = 0000000fdc7ff688 &a[0] = 0000000fdc7ff680 &a[2] = 0000000fdc7ff688
5.7 指针的运算
-
指针可以加上一个整数,相当于地址的偏移(指针指向数组时才有意义)
int a[10]; int *p; p =a; p+2 // p保存a[0]的地址, p+2即为&a[2]
-
两个相同类型的指针可以比较大小(指针指向数组时才有意义)
// 指向前面元素的指针小于指向后面元素的指针
-
相同类型的指针可以做减法,可以用于计算元素个数
int a[10]; int *p,*q; p = &a[0]; q = &a[3]; printf("%d\n",q-p); // 输出为3
-
相同类型的指针可以进行赋值
int a = 100; int *p,*q; p = &a; q = p; printf("%d\n",*q); // 输出为100
5.8 指针数组
-
指针和数组的关系
- 指针可以保存数组元素的地址
- 可以定义数组,用来保存指针变量
指针数组概念:指针数组本身是一个数组,由若干个相同类型的指针变量构成的集合
-
指针数组定义方法
类型说明符 * 数组名 [元素个数] int *p[10]; // 定义一个整型的指针数组p,有10个元素p[0]~p[9],每个元素都是int *类型的变量 int a; p[1] = &a; int b[10]; p[2] = &b[3]; // p[2]、*(p+2)是等价的,都是指针数组的第二个元素
-
指针数组的分类
#include <stdio.h> int main() { // 大多数情况下指针数组用来保存多个字符串 char *name[5] = {"Follow me", "Basic", "Greatwall", "Fortran","Python"}; int i; for (i = 0; i < 5; i++) { printf("%s\n", name[i]); } }
输出结果:
Follow me Basic Greatwall Fortran Python
5.9 指针的指针
指针的指针也叫二级指针,即为指针的地址
定义一个指针变量,占用4个指针,指针变量也有地址编号。
#include <stdio.h>
int main()
{
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);
}
输出结果
a = 100 100 100
a = 0000000000000064 0000000000000064 0000000000000064
5.10 字符串和指针
字符串概念:字符串是以\0
结尾的若干字符的集合
字符串的存储形式:数组、字符串指针、堆
// 定义一个字符数组string,用来存放多个字符,并且用 "I love C" 给string数组初始化,字符串"I LOVE C"存放在string中
1. char string[100] = "I love C";
//定义了一个指针变量str,只能存放字符地址编号,所以"I love C"这个字符串不能存放在str指针变量中。
// str只能存放字符I的地址编号,而"I love C"存放在文字常量区
2. char *str = "I love C"
// 动态申请了10个字节的存储空间,首地址给str赋值
3. char str = (char*)malloc(10*sizeof(char))
注意: str指针指向的内存不能被修改,要看str指向哪里
- str指向文字常量区的时候,内存的内容不能被修改,如第二种方式
- str指向栈、堆、静态全局区的时候,内存的内容是可以被修改的
初始化
-
字符数组、指针指向的字符串,定义时可以直接初始化
char buf_aver[] = "hello world" char *buf_pointer = "hello world"
-
堆中存放的字符串,不能被直接初始化,只能使用strcpy、scanf赋值
char *buf_heap; strcpy(buf_heap,"hello world"); scanf("%s",buf_heap);
使用时赋值
// 字符数组:使用scanf或者strcpy
buf_aver = "hello kitty"; // 错误,因为字符串数组的名字是一个常量,不能不赋值修改
strcpy(buf_aver,"hello kitty"); // 正确
scanf("%s",buf_aver); // 正确
// 指向字符串的指针
buf_point = "hello kitty";
strcpy(buf_point,"hello kitty"); // 错误,只读,不能复制字符串到buf_point指向的内存中
5.11 数组指针
-
二维数组
// 二维数组,有行有列,是多个一维数组的集合 int a[3][5]; // 可以看成是由3个一维数组组成
-
数组指针的概念
数组指针的作用就是可以保存二维数组的首地址。
-
数组指针的定义方法
// 指向的数组的类型(*指针变量名)[指向的数组元素个数] int(*p)[5];
#include <stdio.h> int main(){ int a[3][5];// 定义一个3行5列的二维数组 int (*p)[5];// 定义一个数组指针变量p,p+1跳一个有5位元素的整型数据 printf("a = %p\n",a); // 第0行的行地址 printf("a+1=%p\n",a + 1); // 第一行的行地址,a 和 a+1 相差 5*4 = 20 个地址 p = a; printf("p = %p\n",p); printf("p+1=%p\n",p + 1); // p+1 跳一个有5个整型元素的一维数组 return 0; }
输出结果
a = 000000ca57fff840 a+1=000000ca57fff854 p = 000000ca57fff840 p+1=000000ca57fff854
-
注意:
容易混淆的内容:-
指针数组:是个数组,有若干个相同类型的指针构成的集合
// 数组 p有 10 个 int *类型的指针变量构成,分别是 p[0]~p[9] int *p[10];
-
数组指针:本身是个指针,指向一个数组,加1跳一个数组
int (*p)[10]; // p是个数组指针,p加1指向下个数组,跳 10个整形。
-
指针的牾针:
int **p;//p 是指针的指针 int *q; p=&q;
-
-
数组名字取地址,变成数组指针
int a[10]; // 一维数组的名字取地址,变成一维数组指针,即加1跳一个一维数组 // a+1 跳一个整型元素,是a[1]的地址 // a 和 a+1相差一个元素,4个字节 // &a就变成了一个一维数组的指针,是int(*p)[10]类型 // (&a) + 1 和&a 相差一个数组,即10个元素,40个字节
-
多维数组的指针转换
二维数组的数组名降级问题
// 二维数组的数组名降级问题 // 二维数组的数组名默认是一个行指针,加1保存下一行的首地址 // 二维数组的数据名取*,表示地址的降级,意味着行指针降级为列指针 //一维数组的数组名默认是一个列指针,加1保存下一个元素的地址 //一维数组的数组名取&,则是地址的升级,将列指针升级为行指针,加1保存下一个行元素的首地址 void main() { int a[3][5]; printf("a=%p\n", a); printf("a +1=%p\n", a + 1); // *a变成了第0行第0列元素的地址 printf("*a =%p\n", *a); printf("(*a)+1 =%p\n", (*a) + 1); // 结果为第0行第1列元素的地址 }
输出结果
a=000000EAE5DFFD40 a +1=000000EAE5DFFD54 *a =000000EAE5DFFD40 (*a)+1 =000000EAE5DFFD44
5.12 指针与函数的关系
1. 传数值
可以给一个函数传一个整型、字符型、浮点型数据,也可以传入一个指针
#include <stdio.h>
void myfun1(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);
}
void test1(){
int a = 100, b = 20;
printf("before fun: a =%d,b = %d\n ",a,b);
printf("&a =%p,&b = %p\n ",&a,&b);
myfun1(a,b);
printf("after fun: a =%d,b = %d\n ",a,b);
}
int main(int argc, char const *argv[])
{
/* code */
test1();
return 0;
}
输出结果
before fun: a =100,b = 20
&a = 000000ae377ff77c,&b = 000000ae377ff778
in fun: a =20, b = 100
&a =000000ae377ff750, &b = 000000ae377ff758
after fun: a =100,b = 20
可以发现形参和实参内存地址是相互独立的,互不影响。
函数传参方式是复制传参:将实参的值传递给形参,不管形参怎么改变,跟实参都没有关系。
2. 传地址
#include <stdio.h>
// 函数的传参方式是地址传参
// 形参对保存的地址内容做操作,实参的值也会发生改变
void myfun2(int *p, int *q)
{
int temp;
temp = *p;
*p = *q;
*q = temp;
printf("in fun: *p =%p, *q = %p\n ", p, q);
}
void test1()
{
int a = 100, b = 20;
printf("before fun: a =%d,b = %d\n ", a, b);
printf("&a = %p,&b = %p\n ", &a, &b);
myfun2(&a,&b);
printf("after fun: a =%d,b = %d\n ", a, b);
}
int main(int argc, char const *argv[])
{
/* code */
test1();
return 0;
}
输出结果
before fun: a =100,b = 20
&a = 000000b7fbdff8cc,&b = 000000b7fbdff8c8
in fun: *p =000000b7fbdff8cc, *q = 000000b7fbdff8c8
after fun: a =20,b = 100
总结: 要想修改主调函数中变量的值,必须传变量地址,还得通过*地址
去赋值
注意:
- 如果实参是一个普通变量,地址传参的话就需要形参是一级指针
- 如果实参是一个一级指针,地址传参的话就需要形参是一个二级指针以此类推
3. 传数组
将数组作为参数传递给函数,不存在复制传参和地址传参。
本质都是地址传参,所以函数内部对数组进行改变,则函数执行完毕后,原本的数据也发生改变。
#include <stdio.h>
void fun1(int *p)
{
printf("%d\n", p[2]);
printf("%d\n", *(p + 3));
}
void fun2(int (*p)[4])
{
// p[x][y] 等价于 *((*p+x) + y)
printf("%d\n", p[0][2]);
printf("%d\n", *(*(p + 1) + 3));
}
void fun3(char **q)
{
int i;
for (i = 0; i < 3; i++)
{
printf("%s\n", q[i]);
}
}
// 传一维数组
void test1()
{
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8};
fun1(a);
}
// 传二维数组
void test2()
{
int a[2][4] = {1, 2, 3, 4,
5, 6, 7, 8};
fun2(a);
}
// 传指针数组
void test3()
{
char *p[3] = {"hello world", "world", "kitty"};
fun3(p);
}
int main()
{
test1();
test2();
test3();
return 0;
}
输出结果
3
4
3
8
hello world
world
kitty
4.指针函数
指针函数本质是一个函数,只不过函数的返回值是一个指针
#include <stdio.h>
// 指针函数:指针作为函数的返回值
char *func(){
// 栈区开辟的空间会随着当前代码段的结束而释放空间
// char str[100]="hello world";
// 静态区的空间不会随着代码段的结束而释放空间
static char str[100] = "hello world";
return str;
}
int main()
{
char *p;
p = func();
printf("p = %s\n",p);
return 0;
}
输出结果
p = hello world
5.函数指针
本质是一个指针,指针指向了函数。
在运行程序的时候,系统会将函数加载到内存中,所以函数也有起始地址。
c语言规定,函数的名字就是函数的首地址,即函数的入口地址。
可以定义指针变量,来存放函数的地址,这就是函数指针。
-
函数指针的定义方法
// 返回值类型(*函数指针变量名)(形参列表) int (*p)(int,int) // 定义一个函数指针变量p,p指向函数;其中返回值为整型,两个整型的参数 int max(int x,int y){ } int min(int x,int y){ }
-
调用函数的方法
int max(int x ,int y){} // 如下面的例子 int main(){ int num; num = max(3,5); }
// 可以通过函数指针变量区调用,但是这种方法几乎不怎么用,没有直接使用函数调用方便 int max(int x,int y){ } int main(){ int num; int (*p)(int ,int ); p = max; num = p(3.,5); }
-
函数指针数组
函数指针数组,本质是一个数组,数组里的每一个元素都是函数指针,这个数组就是函数指针数组。
int (*p[10])(int,int); // 了解即可
-
函数指针最常用的地方
函数指针最常用的地方在于将一个函数作为参数传递到另一个函数时候
#include <stdio.h> int add(int x, int y) { /* code */ return x + y; } int sub(int x, int y) { /* code */ return x - y; } int mul(int x, int y) { /* code */ return x * y; } int div(int x, int y) { /* code */ return x / y; } int process(int (*p)(int, int), int a, int b) { /* code */ int ret; ret = (*p)(a, b); return ret; } int main(int argc, char const *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(mul, 2, 3); printf("num = %d\n", num); num = process(div, 2, 3); printf("num = %d\n", num); return 0; }
输出结果
num = 5 num = -1 num = 6 num = 0
5.13 经常容易混淆的指针
-
int *a[10]
: 指针数组,数组中包含10个整型的指针变量,一般可以用来保存字符串 -
int(*a)[10]
: 数组指针,是一个指针变量,占用4个字节,用来存放地址编号。 +1 ,指向下个数组。可以用来保存二维数组的首地址。 -
int **p
:指针的指针,二级指针用来保存一级指针的地址;int **p; int *q; **p = &*q;
-
int *f(void)
:指针函数,声明一个函数,这个函数的返回值为int*
-
int(*f)(void)
:用来存放函数的地址,它指向一个函数
5.14 特殊指针
-
(void *)
指针通用指针,任何类型的指针都可以给
void *
指针变量赋值,主要用在函数的参数和返回值的位置。int *p; void *q; q = p; // 这样操作是可以的,不用强制转换
// memset可以设置字符数组、整型数组、浮点型数组的内容,所以第一个参数就必须使用通过指针 // 它的返回值是s指向的内存的首地址,可能是不同类型的地址。所以返回值也得是通用指针 void *memset(void *s,int c, size_t n):
注意:void*类型的指针变量,也是个指针变量,在32为系统下,占4个字节
-
NULL
空指针,一般用来给指针变量初始化。
// 可以认为p哪里都不指向,也可以认为p指向内存为编号为0的存储单元 // 在p的四个字节中存放的是0x00000000 char *p = NULL;
5.15 main传参
// argc : int 类型的变量,命令终端传入的参数
// argv :是一个指针数组,保存每一个命令终端传入的参数
int main(int argc, char const *argv[])
{
/* code */
return 0;
}
标签:02,变量,17,int,C语言,地址,数组,printf,指针
From: https://www.cnblogs.com/hasaki-yasuo/p/18019413