C语言程序设计(七)
指针
取地址运算
运算符 &
scanf("%d",i); 里的&- 获得变量的地址,它的操作数必须是变量
int i; printf(“%x”, &i); - 地址的大小是否与int相同取决于编译器
int i; printf(“%p”,&i) //取地址
#include <stdio.h>
int main()
{
int i = 0;
int p;
p = (int)&i;
printf("0x%x\n", p);
printf("%p\n",&i);
printf("0x%x\n", p);
return 0;
}
-
格式控制符%p 中的p是pointer(指针)的缩写。指针的值是语言实现(编译程序)相关的,但几乎所有实现中,指针的值都是一个表示地址空间中某个存储器单元的整数。printf 函数族中对于%p一般以十六进制整数方式输出指针的值,附加前缀0x 。 -
%p是打印地址的, %x是以十六进制形式打印, 完全不同!在32位下可能一样,但是在64位下结果会不一样, 所以打印指针老老实实用%p
&不能取的地址
- &不能对没有地址的东西取地址(即它右边一定是明确的变量, 其他的取不了地址)
- &(a+b)? 不能取
- &(a++)? 不能取
- &(++a)? 不能取
试试这些&
-
变量的地址 : printf(“%p”,&i) -
相邻的变量的地址 : 存储在连续的内存空间中 -
&的结果的sizeof
#include <stdio.h>
int main(void)
{
int i = 0;
int p;
printf("0x%x\n", &i);
printf("%p\n", &i);
printf("%lu\n", sizeof(int));
printf("%lu\n", sizeof(&i));
return 0;
}
输出结果:
0x62fe1c
000000000062FE1C
4
8
#include <stdio.h>
int main(void)
{
int a[10];
printf("%p\n", &a);
printf("%p\n", a);
printf("%p\n", &a[0]);
printf("%p\n", &a[1]);
return 0;
}
输出:
000000000062FDF0
000000000062FDF0
000000000062FDF0
000000000062FDF4
- 相邻的数组的内存地址相差4; 一个int型所占的空间(4字节)
scanf
- 如果能够将取得的变量的地址传递给一个函数,能否通过这个地址在那个函数内访问这个变量? 答案是:可以的
scanf(“%d”, &i); scanf() 的原型应该是怎样的?我们需要一个参数能保存别的变量的地址,如何表达能够保存地址的变量? 这时候就要用到指针.
指针
- 指针类型变量就是保存地址的变量
int i; int* p = &i; //p为指针型变量, 存放int i 的内存地址,就是指向i. 其中* 可以靠近int或者靠近p都是一样的int* p,q; //和int *p,q; 表达的意思一样,都是p是指针变量,而q不是.int *p,q; //和int* p,q; 表达的意思一样,都是p是指针变量,而q不是.int *p,q; int *p, *q; //这样才表示p和q都是指针型变量.
指针变量
- 变量的值是内存的地址
- 普通变量的值是实际的值
- 指针变量的值是具有实际值的变量的地址
作为参数的指针
- void f(int *p);
- 在被调用的时候得到了某个变量的地址:
- 在函数里面可以通过这个指针访问外面的这个i
#include <stdio.h>
void f(int *p);
int main(void)
{
int i = 6;
printf("&i=%p\n", &i);
f(&i);
return 0;
}
void f(int *p)
{
printf(" p=%p\n", p);
}
输出结果:
&i=000000000062FE1C
p=000000000062FE1C
都是一样的
访问那个地址上的变量*
* 是一个单目运算符,用来访问指针的值所表示的地址上的变量
- 比如:
int *p = &a; //是从右往左结合的 - 可以做右值也可以做左值
#include <stdio.h>
void f(int *p);
void g(int k);
int main(void)
{
int i = 6;
printf("&i=%p\n", &i);
f(&i);
g(i);
return 0;
}
void f(int *p)
{
printf(" p=%p\n", p);
printf("*p=%p\n", *p);
*p = 26;
}
void g(int k)
{
printf(" k=%d\n",k);
}
输出结果:
&i=000000000062FE1C
p=000000000062FE1C
*p=0000000000000006
k=26
左值之所以叫左值
- 是因为出现在赋值号左边的不是变量,而是值,是表达式计算的结果:
a[0] = 2 ;*p = 3; //必须定义了指针变量,而且指向另一个变量,才能进行赋值a = b; //是将变量b的值而不是变量b赋给变量a - 是特殊的值,所以叫做左值
指针的运算符&*
-
互相反作用 -
*&yptr -> * (&yptr) -> * (yptr的地址)-> 得到那个地址上的变量 -> yptr
- 比如:a是b的指针变量;
a == &b; *a == b; 即*&b==b; -
&*yptr -> &(*yptr) -> &(y) -> 得到y的地址,也就是yptr -> yptr -
比如:a是b的指针变量;*a == b; &b ==a; 即&*a == a;
传入地址
- 为什么
int i; scanf(“%d”, i); 编译没有报错?
答:原因是int型和指针型在32位的操作系统上他们的长度是一致的,编译器认为它的值是内存地址,所以可以编译过关;但运行一定出错,因为它的值不是它的内存地址,系统会将你要输入的值写到其他地方(就是把int i的值当做内存地址进行写入),而你的i变量上就没有你输入的值,从而报错。
指针运用场景(一)
void swap(int *pa, int *pb)
{
int t = *pa;
*pa = *pb;
*pb =t;
}
#include <stdio.h>
void swap(int *pa, int *pb);
int main()
{
int a = 5;
int b = 6;
printf("交换前: a=%d, b=%d\n", a,b);
swap(&a, &b);
printf("交换后: a=%d, b=%d\n", a,b);
return 0;
}
void swap(int *pa, int *pb)
{
int t = *pa;
*pa = *pb;
*pb =t;
}
输出结果:
交换前: a=5, b=6
交换后: a=6, b=5
-
误区: -
void swap(int a, int b)
{
int t = a;
a = b;
b =t;
}
-
这样的交换并不能达到交换a,b两个值的目的,因为你传进去的只是变量的值而不是变量, 而函数里面它又重新开辟有一个新的空间,即开辟新的a,b两个内存空间,然后里面的传值只是传到新的地址空间,并没有改变外面原来的a,b变量的值。*而即使你的外面的变量名和函数里面的变量名相同也是一样达不到交换的目的,他们只是相同变量名存放在不同的内存地址。*因此必须用到指针变量来进行交换,本质就是把要交换的变量的值进行修改,就是要找到那个变量的地址,在存放的地址上进行修改值。
指针应用场景(二)
- 函数返回多个值,某些值就只能通过指针返回
- 传入的参数实际上是需要保存带回的结果的变量
#include <stdio.h>
void minmax(int a[], int length, int *min, int *max);
int main(void)
{
int a[] = {1, 2, 5, 4, 7, 1, 6, 9, 8};
int min, max;
int length = sizeof(a)/sizeof(a[0]);
minmax(a, length, &min, &max);
printf("min = %d, max = %d\n", min, max);
return 0;
}
void minmax(int a[], int length, int *min, int *max)
{
int i;
*min = *max = a[0];
for(i=1; i<length; i++){
if(*min>a[i]){
*min = a[i];
}
if(*max<a[i]){
*max = a[i];
}
}
}
输出:
min = 1, max = 9
- 函数返回运算的状态,结果通过指针返回
- 常用的套路是让函数返回特殊的不属于有效范围内的值来表示出错:
#include <stdio.h>
int divide(int a, int b, int *c);
int main()
{
int a = 5;
int b = 2;
int c;
if (divide(a, b, &c)){
printf("%d/%d=%d\n", a, b, c);
}
}
int divide(int a, int b, int *c)
{
int ret;
if(b==0){
ret = 0;
}else{
*c = a/b;
ret = 1;
}
return ret;
}
- 但是当任何数值都是有效的可能结果时,就得分开返回了
- 后续的语言(C++, Java)采用了异常机制来解决这个问题
指针最常见的错误
- 定义了指针变量,还没有指向任何变量,就开始使用指针
#include <stdio.h>
int main()
{
int *a;
int b = 1;
printf(" b=%d\n",b);
a = &b;
*a =2;
printf(" a=%d\n",a);
printf("*a=%d\n",*a);
printf(" b=%d\n",b);
printf("&b=%d\n",&b);
b = 3;
printf(" b=%d\n",b);
printf("&b=%d\n",&b);
}
输出结果:
b=1
a=6487572
*a=2
b=2
&b=6487572
b=3
&b=6487572
即必须将指针变量指向某一个变量的时候才能对其进行操作.
指针与数组
- 传入函数的
数组 成了什么?
- 函数参数表中的数组实际上是指针
sizeof(a) == sizeof(int*) //a的长度等于int*的长度;在汇编代码中a相当于数组a[]的头指针.- 但是可以用数组的运算符[]进行运算
#include <stdio.h>
void minmax(int a[], int length, int *min, int *max);
int main(void)
{
int a[] = {1, 2, 5, 4, 7, 1, 6, 9, 8};
int min, max;
printf("main minmax sizeof(a)=%lu\n", sizeof(a));
printf("main a=%p\n", a);
int length = sizeof(a)/sizeof(a[0]);
minmax(a, length, &min, &max);
printf("min = %d, max = %d\n", min, max);
printf("a[0]=%d\n", a[0]);
return 0;
}
void minmax(int a[], int length, int *min, int *max)
{
int i;
printf("minmax sizeof(a)=%lu\n", sizeof(a));
a[0] = 100;
*min = *max = a[0];
for(i=1; i<length; i++){
if(*min>a[i]){
*min = a[i];
}
if(*max<a[i]){
*max = a[i];
}
}
}
输出结果:
main minmax sizeof(a)=36
main a=000000000062FDF0
minmax sizeof(a)=8
min = 1, max = 100
a[0]=100
- 如果将
void minmax(int a[], int length, int *min, int *max) 改成void minmax(int *a, int length, int *min, int *max) ,运行结果一模一样,这说明a[]就是一个指针变量. 所以在函数参数中不能传入a[n]格式,而是直接将a[]数组以a传入,函数中的参数接收不了整个数组,但可以接收指针,而数组指针更可以。
#include <stdio.h>
void p(int *a);
int main(void)
{
int a = 12;
printf("main a=%d\n", a);
printf("main a=%p\n", a);
p(&a);
printf("main a=%d\n", a);
return 0;
}
void p(int *b)
{
b[0] = 100;
b[2] = 2;
printf("b[0]=%d\n", b[0]);
printf("b[2]=%d\n", b[2]);
printf("b=%d\n", b);
}
输出结果:
main a=12
main a=000000000000000C
b[0]=100
b[2]=2
b=6487580
main a=100
数组参数
- 以下四种函数原型是等价的:
int sum(int *ar, int n); int sum(int *, int); int sum(int ar[], int n); int sum(int [], int);
数组变量是特殊的指针
- 数组变量本身表达地址,所以
int a[10]; int*p=a; // 无需用& 取地址 - 但是数组的单元表达的是变量,需要用&取地址
a == &a[0] //即数组的头指针也就是数组的第一个元素的内存地址 [] 运算符可以对数组做,也可以对指针做:
- *运算符可以对指针做,也可以对数组做:
- 数组变量是
const (常量)的指针,所以两个数组间不能被赋值
int a[] <==> int * const a= ….
附加知识点*
指针与const(只适用于C99)
指针是const
- 表示一旦得到了某个变量的地址,不能再指向其他变量
int * const q = &i; // q 是 const, 即q的值不能被改变,也就是i的地址*q = 26; // OKq++; // ERROR 是错误的; 不能将q的值进行运算,因为q是const
所指是const(指针所指的变量是const)
- 表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const)
const int *p = &i; //将变量i的地址赋给*p
*p = 26; // ERROR!不能; (*p) 是 const; 不能通过p进行修改变量i的值,因为`(*p)是const, 而i不是const,可以进行i++运算
i = 26; //OK是可以进行的
p = &j; //OK是可以进行的
这些是啥意思?
-
int i; -
const int* p1 = &i; //p1指针所指的东西不能被修改 -
int const* p2 = &i; //p2指针所指的东西不能被修改 -
int *const p3 = &i; //p3指针不能被修改 判断哪个被const了的标志是const在*的前面还是后面
const在* 前面是指针所指的东西不能被修改; const在* 后面是指针不能被修改
转换
-
总是可以把一个非const的值转换成const的 void f(const int* x);
int a = 15; f(&a); // ok可以
const int b = a; f(&b); // ok可以
b = a + 1; f(&b); // Error!不能, 因为 a+1不是const
-
当要传递的参数的类型比地址大的时候,这是常用的手段:既能用比较少的字节数传递值给参数,又能避免函数对外面的变量的修改 (比如传入一个十分大的数组进函数, 实际是传入一个指针进去)
const数组
const int a[] = {1,2,3,4,5,6,}; //这是一个const数组;必须通过这种方式进行初始化,否则写出来后就不能进行赋值了.- 数组变量已经是const的指针了,这里的const表明数组的每个单元都是const int
- 所以必须通过初始化进行赋值
保护数组值
- 因为把数组传入函数时传递的是地址,所以那个函数内部可以修改数组的值
- 为了保护数组不被函数破坏,可以设置参数为const
int sum(const int a[], int length); //这样传进函数内就不会被修改了
指针运算
指针是可计算的
指针计算
-
这些算术运算可以对指针做:
#include <stdio.h>
int main(void)
{
char ac[] = {0, 1, 2, 3, 4, 5, };
char *p = ac;
printf("p =%p\n", p);
printf("p+1 =%p\n", p+1);
printf("*(p+1)=%d\n",*(p+1));
int ai[] = {0, 1, 2, 3, 4, 5, };
int *q = ai;
printf("q =%p\n",q);
printf("q+1 =%p\n",q+1);
printf("*(q+1)=%d\n",*(q+1));
return 0;
}
输出结果: p =000000000062FE00
p+1 =000000000062FE01
*(p+1)=1
q =000000000062FDE0
q+1 =000000000062FDE4
*(q+1)=1
所以sizeof(char) = 1; sizeof(int) = 4;
#include <stdio.h>
int main(void)
{
char ac[] = {0, 1, 2, 3, 4, 5, };
char *p = &ac[0];
char *p1 = &ac[5];
printf("p =%p\n", p);
printf("p+1 =%p\n", p+1);
printf("p1 - p=%d\n",p1-p);
int ai[] = {0, 1, 2, 3, 4, 5, };
int *q = ai;
int *q1 = &ai[5];
printf("q =%p\n",q);
printf("q+1 =%p\n",q+1);
printf("q1 - q=%d\n",q1-q);
return 0;
}
输出结果: p =000000000062FDF0
p+1 =000000000062FDF1
p1 - p=5
q =000000000062FDD0
q+1 =000000000062FDD4
q1 - q=5
-
可以看出指针的加减并不是以内存地址的字节来输出运算结果,而是通过加减所得的字节长除以类型变量所占的字节长得出的运算结果, 并不是简单的加减一。 -
相同内容的数组,不同类型所占的空间不一样;比如int型数组和char型数组,即使元素个数相等,元素值相同,但数组所占空间大小不一样。 -
注意:指针可以做加减法, 但是没有乘除法。
*p++
- 取出p所指的那个数据来,完事之后顺便把p移到下一个位置去
* 的优先级虽然高,但是没有++ 高- 常用于数组类的连续空间操作
- 在某些CPU上,这可以直接被翻译成一条汇编指令
#include <stdio.h>
int main(void)
{
char ac[] = {0, 1, 2, 3, 4, 5, };
char *p = &ac[0];
int i;
for(i=0; i<sizeof(ac)/sizeof(ac[0]); i++){
printf("%d ",ac[i]);
}
printf("\n");
int ai[] = {0, 1, 2, 3, 4, 5, -1};
int *q = ai;
{
for(q=ai;*q!= -1; ){
printf("%d ",*q++);
}
printf("\n");
}
{
int *q = ai;
while(*q!= -1){
printf("%d ",*q++);
}
}
return 0;
}
输出结果:
0 1 2 3 4 5
0 1 2 3 4 5
0 1 2 3 4 5
指针比较
<, <=, ==, >, >=, != 都可以对指针做- 比较它们在内存中的地址
- 数组中的单元的地址肯定是线性递增的
0地址
- 当然你的内存中有0地址,但是0地址通常是个不能随便碰的地址
- 所以你的指针不应该具有0值
- 因此可以用0地址来表示特殊的事情:
- 返回的指针是?无效的
- 指针没有被真正初始化(先初始化为0)
- NULL(必须全是大写)是一个预定定义的符号,表示0地址
指针的类型
指针的类型转换
用指针来做什么
- 需要传入较大的数据时用作参数
- 传入数组后对数组做操作
- 函数返回不止一个结果
- 动态申请的内存…
动态内存分配
输入数据
- 如果输入数据时,先告诉你个数,然后再输入,要记录每个数据
- C99可以用变量做数组定义的大小,C99之前呢?必须用动态内存分配
- 用
malloc 函数
int *a = (int*)malloc(n*sizeof(int));
malloc
#include <stdlib.h>
void* malloc(size_t size);
- 在使用malloc前, 需要加入头文件
#include <stdlib.h> - 向
malloc 申请的空间的大小是以字节 为单位的 - 返回的结果是
void* ,需要类型转换为自己需要的类型*
*(int*)malloc(n*sizeof(int))
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int number;
int* a;
int i;
printf("输入数字的数量:");
scanf("%d", &number);
a = (int*)malloc(number*sizeof(int));
printf("请依次输入数字,以空格隔开:");
for (i=0; i<number; i++){
scanf("%d",&a[i]);
}
for ( i=number-1; i>=0; i--){
printf("%d ",a[i]);
}
free(a);
return 0;
}
输出结果:
输入数字的数量:5
请依次输入数字,以空格隔开:1 2 3 4 5
5 4 3 2 1
没空间了?
- 如果申请失败则返回
0 ,或者叫做NULL - 你的系统能给你多大的空间?
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
void* p;
int cnt = 0;
while ( (p=malloc(100*1024*1024)) ) {
cnt++;
}
printf("分配了%d00MB的空间\n", cnt);
free(p);
return 0;
}
输出:
分配了33600MB的空间
每个计算机能分配的最大空间都不一定相同
free()
- 把申请得来的空间还给“系统”
- 申请过的空间,最终都应该要还
- 出来混的,迟早都是要还的 所以一定要释放申请的内存空间
- 只能还申请来的空间的首地址(原地址) , 还回去的首地址只能是之前申请的首地址
void* p = 0;
p=malloc(100*1024*1024);
p++;
free(p);
return 0;
- ?
void* p = 0 ; //定义指针的时候好习惯是先初始化它为0; - 其次
free(NULL); 是指啥操作也不做,单纯是一种习惯
常见问题
-
申请了没free—>长时间运行内存逐渐下降 ,又称内存垃圾或内存漏洞,程序申请的内存总是有限的
-
已经free过了再free(也会崩溃) -
地址变过了,直接去free ; (比如:p++的例子)
函数间传递指针
好的模式
- 如果程序中要用到动态分配的内存,并且会在函数之间传递,不要让函数申请内存后返回给调用者
- 因为十有八九调用者会忘了free,或找不到合适的时机来free
- 好的模式是让调用者自己申请,传地址进函数,函数再返回这个地址出来
#include <stdio.h>
#include <stdlib.h>
int* init(int a[], int length);
int* print(int a[], int length);
int main()
{
const int MAX_SIZE = 1000;
int size;
do{
printf("输入元素的数量(0,1000):");
scanf("%d",&size);
}while(size<=0 && size>MAX_SIZE);
int* a = (int*)malloc( size*sizeof(int) ) ;
print(init(a,size),size);
free(a);
}
int* init(int a[], int length)
{
int i;
for (i=0; i<length; i++){
a[i] = i;
}
return a;
}
int* print(int a[], int length)
{
int i;
for(i=0; i<length; i++){
printf("%d\t", a[i]);
}
printf("\n");
return a;
}
输出结果:
输入元素的数量(0,1000):20
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
15 16 17 18 19
函数返回指针?
- 返回指针没问题,关键是谁的地址?
- 本地变量(也叫局部变量; 包括参数)?函数离开后这些变量就不存在了,指针所指的是不能用的内存
- 传入的指针?没问题
- 动态申请的内存?没问题
- 全局变量—>它的作用域默认是整个程序
函数返回数组?
-
如果一个函数的返回类型是数组,那么它实际返回的也是数组的地址 -
如果这个数组是这个函数的本地变量,那么回到调用函数那?里,这个数组就不存在了 -
所以只能返回
- 传入的参数:实际就是在调用者那里
- 全局变量或动态分配的内存
-
和返回指针是一样的(函数返回数组的本质就是函数返回指针)
注意 : 除非函数的作用是分配空间, 否则不要在函数中malloc然后传出去用
局部变量和全局变量; 形参和实参
拓展:
局部变量:定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。
-
在 main 函数中定义的变量也是局部变量,只能在 main 函数中使用;同时,main 函数中也不能使用其它函数中定义的变量。main 函数虽然是主函数,但也是一个函数,与其它函数地位平等。 -
形参变量、在函数体内定义的变量都是局部变量。实参给形参传值的过程也就是给局部变量赋值的过程。 -
==局部变量可以与全局变量同样命名。==也可以在不同的函数中使用相同的变量名,它们表示不同的数据,分配不同的内存,互不干扰,也不会发生混淆。但是优先级是局部变量优先 -
在语句块中也可定义变量,它的作用域只限于当前语句块。
全局变量 :在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。
举个栗子:
#include <stdio.h>
int n = 0;
void func1(){
int n = 1;
printf("func1 n: %d\n", n);
}
void func2(int n){
printf("func2 n: %d\n", n);
}
void func3(){
printf("func3 n: %d\n", n);
}
int main(){
int n = 2;
func1();
func2(n);
func3();
{
int n = 3;
printf("block n: %d\n", n);
}
printf("main n: %d\n", n);
return 0;
}
输出:
func1 n: 1
func2 n: 2
func3 n: 0
block n: 3
main n: 2
形参变量要等到函数被调用时才分配内存,调用结束后立即释放内存。这说明形参变量的作用域非常有限,只能在函数内部使用,离开该函数就无效了。所谓作用域(Scope),就是变量的有效范围。
C语言函数的参数会出现在两个地方,分别是函数定义处和函数调用处,这两个地方的参数是有区别的。
形参(形式参数):
在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参。
实参(实际参数):
函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参。
形参和实参的功能是传递数据,发生函数调用时,实参的值会传递给形参。
|