学习了《C程序设计(第四版)-谭浩强》指针章节,做此笔记。
一、
int i = 1;
int *j = &i;
一个变量的地址 称为该变量的“指针”。如果有一个变量j专门用来存放另一个变量的地址(即指针),则它称为“指针变量”。指针变量用来存放地址。指针变量的值是地址(即指针)。
指针变量存放指针。
指针变量存放地址。
指针是具体的一个地址。指针变量是存放地址的一个变量。
二、
I
int i = 0;
int * pointer; //读作:定义了一个指针变量pointer 而不是 *pointer
//这里表示定义了一个指针变量pointer
//*表示该变量pointer是一个指针变量
pointer = &i; //使pointer指向变量i;即表示把i的地址存放在pointer中
printf( “%d”, *pointer );
// “ * ”表示“指向”。*pointer表示指针变量pointer所指向的变量
可以简称 * 为取数符。*p表示指针变量p指向的对象
& 为取址符
II
一个变量的指针的含义包括两个方面:
一是 以存储单元编号表示的地址;
二是 指向的存储单元的数据类型。
在说明变量类型时不能一般的说“b是一个指针变量”,而是完整地说“b是指向整型数据的指针变量”。
III
使用方法:int i=1; int * j = &i;
或者 int i=1; int * j = &i; *j = 5; //表示把5赋值给i
或者 printf(“%o”,j); //以八进制形式输出指针变量j的值,即i的地址
IV指针变量作为函数参数
void swap(int * p1, int * p2){
// 形参获得实参的值,即p1 = &a。依然是“值传递”,只是传递的值是地址值
int temp;
* p1 = * p2; // *p1就是指针变量p1指向的变量,也就是a
* p2 = temp;
}//p1, p2的值从始至终都未变。改变的是a和b的值。
int main(){
int a=1;b=2;
int * pointer1;
int * pointer2;
pointer1 = &a;
pointer2 = &b;
swap( pointer1, pointer2 ); //传递的就是变量a和b的地址(指针)
return 0;
}
文字描述:
1.在主函数中设n个变量,用n个指针指向它们
2.在自定义函数中,有n个指针形参。在函数内,改变这n个指针形参指向的变量的值
3.在主函数中调用自定义函数,将这n个指针变量作为实参,将他们的值(指向参数的地址)传给自定义函数的形参
4.在执行函数的过程中,通过形参指针变量,改变了指针变量指向的n个变量的值
5.主函数中就可以直接使用改变了值的变量。
三、通过指针引用数组
I
int a[10]={0};
int * p = a; // int * p = &a[0];
在指针指向数组元素时,可以对指针进行加减运算。
* ( p+i ) * ( a+i ) a[ i ] 三者等价
在输入时可以这样写:
for( int * p=a; p<(a+10); p++){
scanf("%d",p); //可以提高执行效率
}
指针变量也可以带下标,例如p[ i ],会被处理成 *( p+i )。
不同于数组名永远指向数组第一个元素,需要注意p的当前值是什么,它不一定指向第一个元素。
int a[6] = {0,1,2,3,4,5};
int *p= a+3;
printf("%d",p[2]); //p[2]就是a[3+2]就是元素5
* p ++ 同等优先级,结合方向为自右向左。
等价于 *(p++)。先引用p的值实现 *p运算,再使p自增1
* p ++ 与 * ++ p不同。若p=a,则前者为a[0]后者为a[1]
II数组名作函数参数
int main(){
int a[10]={0};
func(a,10);
}
func( int arr[], int n){}
func( int * p, int n){} //二者等价
例子中,实参数组名a代表的是数组元素首地址。而形参是用来接收一个数组元素首地址的。所以形参必须是一个指针变量(只有指针变量才可以存放地址)。实际上,C编译器都是将形参数组名arr作为指针变量来处理的。所以上述二者是等价的。
且形参数组不必指定数组大小,因为它实际上是一个指针变量,并不是开辟一个真正的空间。
当arr接收了实参数组的首地址之后,arr就指向了实参数组首元素。
*( arr+1 )、arr[1] 恒等价,都可以使用来表示数组中第二个元素。
从应用上看,一个形参数组,从实参数组那里得到起始地址。因此形参数组与实参数组共占同一段内存单元。在调用函数期间,如果改变了形参数组的值,也就改变了实参数组的值。
实参数组名代表一个固定地址,是指针常量,不可被改变。而形参数组会被按指针变量来处理。
III通过指针引用多维数组
int a[3][4] = { {1,3,5,7},
{9,11,13,15},
{17,19,21,23}};
a代表首行的首地址;a+1代表序号为1的行的首地址;
如果a为2000即首行的首地址,则a+1为2000+4*4=2016即下一行的首地址。
a+1指向a[1],即a+1的值是a[1]的首地址,即a+1=&a[1]
而a[1]的值为&a[1][0],a[0]的值为&a[0][0]
而a[0]+1的值为&a[0][1]为2000+4,a[0]+2的值为&a[0][2]为2000+4*2
对比一维数组,a[0]与*(a+0)等价,
二位数组中a[0]+1与*(a+0)+1等价,a[0]+2与*(a+0)+2等价
欲得到a[0][2]的值,只需要 *(&a[0][2]) 即 *(a[0]+2) 即 *(*(a+0)+2)
int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
int (*p)[4], i, j;
//p定义为指向包含4个整型元素的一维数组的指针变量。p的基类型是指向一维数组的指针变量,长度为16字节。(*p)有4个整型元素
p = a;
scanf("%d %d",&i,&j);
printf("[%d,%d] = %d", i, j, *(*(p+i)+j)); //输出 a[i][j] 的值
//p加i是以p的基类型的长度(16字节)为单位加i的。
//经过*( p+i )运算得到 a[i] 即&a[i][0],这时*( p+i )已经转换为指向列元素的指针了;再加j就是以元素长度4为单位加j
//虽然p+i和*(p+i)都表示第i行的首地址,但是前者是按行(一维数组)递增的,后者是按列(元素)递增的。
IV用指向二维数组的指针作函数参数
例题:一个班级3个学生各4门课程,计算总平均分数以及第n个学生的成绩。
#include<stdio.h>
void average(float * p, int n){
float * p_end;
float sum=0,aver;
p_end = p+n-1;
while( p<=p_end ) sum += *p++;
aver = sum/n;
printf("avergae = %.2f\n",aver);
}
void search(float (*p)[4], int n){
printf("The scores of No.%d are:\n",n);
for(int i=0;i<4;i++) printf("%.2f ",*(*(p+n)+i));
printf("\n");
}
int main(){
float score[3][4] = {
{65,67,70,60},
{80,87,90,81},
{90,99,100,98}};
average(*score,12); //求12个分数的平均分
search(score,2); //求序号为2的学生的成绩
return 0;
}
分析:
average(*score,12); 实参*score即score[0]即&score[0][0]即score[0][0]的地址
average(float * p, int n){} 形参p定义为指针,传入实参后,p指向score[0][0]。*p就是score[0][0],*(p+1)就是score[0][1],*(p+n-1)就是score[2][3]就是最后一个元素。
search(score,2); 实参score代表数组0行的起始地址即&score[0]
search(float (*p)[4], int n){} 形参p的类型是float(*)[4],p指向包含4个整型元素的一维数组。p的基类型是指向一维数组的指针变量。传入实参后,p指向score[0]。p+n表示score[n]的起始地址,*(p+n)表示score[n][0]的起始地址,*(p+n)+i表示score[n][i]的地址,*(*(p+n)+i)表示score[n][i]的值。n为2,i从0到3,这样就输出了score[2][0]-score[2][3]的值。
注意:int*型的指针(指向元素地址)与 int(*)[4]型的指针(指向一维数组)的区别
例题:在上题基础上,查找有一门以上不及格的同学,输出他们的全部课程成绩。
#include<stdio.h>
void search(float (*p)[4],int n){
for(int j=0;j<n;j++){
int flag=0;
for(int i=0;i<4;i++) if( *(*(p+j)+i) <60 ) flag = 1;
if(flag==1){
printf("No.%d fails, his scores are:\n",j+1);
for(int i=0;i<4;i++) printf("%.2f ",*(*(p+j)+i));
printf("\n");
}
}
}
int main(){
float score[3][4] = {
{65,57,70,60},
{58,87,90,81},
{90,99,100,98}};
search(score,3); //求序号为2的学生的成绩
return 0;
}
分析:实参score和形参p的类型是相同的。实参传递的是score[0]的地址,形参p就指向第一行。p+j是score[j]的起始地址,*(p+j)是score[j][0]的起始地址,*(p+j)+i是score[j][i]的地址,*(*(p+j)+i)就是score[j][i]的值。
四、通过指针引用字符串
I
char str[] = "I Love China!"; //长度为14,最后一个字节存放结束符'\0'
printf("%s\n",str); //%s格式声明输出str,可以输出整个字符串
printf("%c\n",str[2]); //%c格式输出一个字符数组元素
数组名str代表字符数组首元素的地址。用字符指针变量指向一个字符串常量,通过字符指针变量引用字符串常量。
还可以:char * str = "I Love China!";
printf("%s\n",str);
不定义字符数组,而只定义了一个 char * 型变量(字符型指针变量),用它指向字符串常量中第一个字符,而不是指向多个字符数据,更不是把整个字符串放到str中(指针变量只能存放地址),也不是把字符串赋给 *str。只是把字符串的第一个字符的地址赋值给指针变量str。
%s是输出字符串时所用的格式符,在输出项中给出字符指针变量名str,系统会输出str指向的字符,然后自动使str加1,使之指向下一个字符再输出,再加1…知道遇到字符串结束标志'\0'为止。
例题:把字符串a复制给字符串b
#include<stdio.h>
int main(){
char a[]="I am a boy.",b[20];
int i;
for(i=0;*(a+i)!='\0';i++) *(b+i) = *(a+i);
*(b+i)='\0';
printf("string b is:%s\n",b);
}
#include<stdio.h>
int main(){
char a[]="I am a boy.",b[20],*p1,*p2;
p1 = a,p2=b;
while(*p1!='\0') *p2++ = *p1++;
*p2 = '\0';
printf("string b is:%s\n",b);
}
II字符指针作函数参数
如果想把一个字符串从一个函数“传递”到另一个函数,可以用地址传递的办法,即用字符数组名作参数,也可以用字符指针变量作参数。在被调用的函数中可以改变字符串的内容﹐在主调函数中可以引用改变后的字符串。
(1)使用字符数组名作为函数实参
#include<stdio.h>
void copy_string(char from[], char to[]){//形参数组不需要指定大小
//因为它实际上是一个指针变量,并没有开辟新的空间
int i=0;
while(from[i]!='\0') to[i] = from[i++];
to[i]='\0';
}
int main(){
char a[]="I am a student.";
char b[]="You are a student.";
printf("string a is:%s\nstring b is:%s\n",a,b);
printf("copy string a to string b:\n");
copy_string(a,b); //使用字符数组名作为函数实参
printf("string a is:%s\nstring b is:%s\n",a,b);
return 0;
}
(2)使用字符指针变量作函数实参
#include<stdio.h>
void copy_string(char from[], char to[]){
int i=0;
while(from[i]!='\0') to[i] = from[i++];
to[i]='\0';
}
int main(){
char a[]="I am a student.";
char b[]="You are a student.";
printf("string a is:%s\nstring b is:%s\n",a,b);
printf("copy string a to string b:\n");
char *from = a, *to = b;
copy_string(from,to); //唯一不同之处
printf("string a is:%s\nstring b is:%s\n",a,b);
return 0;
}
(3)使用字符指针变量作函数实参和形参
#include<stdio.h>
void copy_string(char *from, char *to){
//方法一
int i=0;
while(from[i]!='\0') to[i] = from[i++];
to[i]='\0';
//方法二
int i=0;
while(*(from+i)!='\0') *(to+i) = *(from + i++);
*(to+i)='\0';
//方法三
while(*from!='\0') *to++ = *from++;
*to='\0';
//方法四
while((*to++ = *from++)!='\0');
//方法五
while(*to++ = *from++);// '\0'的ASCII码为0
//方法六
for(;*to++=*from++;);
}
int main(){
char *a="I am a student.";
char b[]="You are a student.";
char *p = b;
printf("string a is:%s\nstring b is:%s\n",a,b);
printf("copy string a to string b:\n");
copy_string(a,p);
printf("string a is:%s\nstring b is:%s\n",a,b);
return 0;
}
III使用字符指针变量与字符数组的比较
(1)字符指针变量存放的是第一个字符的地址。字符数组由若干个字符组成
(2)可以直接对字符指针变量赋值,而不能对数组名赋值
char * a, str[10];
a = "I am a student."; //合法
str = "I am a student."; //非法
str[0] = 'I'; //合法
(3)存储单元内容。编译时为字符数组分配若干个存储单元以存放各元素的值。而对字符指针变量,只分配一个存储单元(4个字节)。
(4)如果定义了字符数组而未进行赋值,这时数组中的元素值时无法预料的,可以引用这些值但是无意义。如果定义了指针变量而未进行赋值指向,它指向的内存单元是无法预料的。
(5)指针变量的值是可以改变的,数组名代表一个固定的值(首元素地址)不能改变。
#include<stdio.h>
int main(){
char *a="I am a student.";
a += 7;
printf("%s\n",a); //输出student.
return 0;
}
(6)数组中各元素的值可以改变,而不能通过指针变量改变它指向的字符串常量。
char a[] = "I am a student.";
a[2]= 'd'; //合法
char * b = "I am a student.";
b[2]= 'd'; //非法
(7)用指针变量指向一个格式字符串,可以代替printf中的格式字符串
#include<stdio.h>
int main(){
char *format="a = %s\nb = %s\n";
char a[] = "Hello", b[] = "World!";
printf(format,a,b);
return 0;
}
五、指向函数的指针
I什么是函数指针
定义一个函数,在编译时系统分配一段存储空间。这段存储空间的起始地址(入口地址)称为这个函数的指针。
int (*p)(int, int); //定义p是一个指向函数的指针变量,它可以指向函数的类型为整型且有两个整型参数。
II用函数指针变量调用函数
调用函数可以用函数名或者函数指针。
int max(int x, int y){}
int main(){
int a=1, b=2;
int (*p)(int, int); //定义函数指针
p=max; //赋值,只给出函数名不必给出参数
int c = (*p)(a, b); //调用函数,等价于 int c = max(a,b);
return 0;
}
调用 *p 就是调用 max函数
函数指针变量不能加减运算
函数指针变量可以先后被赋值不同的函数
III用函数指针变量作函数参数
函数指针变量的一个重要用途就是把函数的地址作为参数传递到其他函数。
void func(int (*p1)(int), int (*p2)(int, int)){
int x=1, y=2;
int a = (*p1)(x);
int b = (*p2)(x, y);
}
例题:有两个整数a和b,由用户输入1、2或3来选择程序输出最大者、最小者或者二者之和。
#include<stdio.h>
int max(int x, int y){
printf("Max is ");
return x>y?x:y;
}
int min(int x, int y){
printf("Min is ");
return x<y?x:y;
}
int add(int x, int y){
printf("Sum is ");
return x+y;
}
void func(int x, int y, int(*p)(int, int)){
printf("%d\n",(*p)(x, y));
}
int main(){
int x=10,y=20,n,(*p)(int,int);
scanf("%d",&n);
if(n==1) p=max;
else if(n==2) p=min;
else p=add;
func(x,y,p);
return 0;
}
疑惑:调用函数指针变量p的时候,p(x,y)或者(*p)(x,y)都可以执行成功,并不是按照书上说的,只能(*p)(x,y)。不知道为什么?
例题:编写求以下定积分的通用函数。
,
,
,
,
#include<stdio.h>
#include <cmath>
float f1(float x){
return x+x*x/2;
}
float f2(float x){
return x*x+3*x;
}
float f3(float x){
return exp(x)+x;
}
float f4(float x){
return pow((1+x),3)/3;
}
float f5(float x){
return pow(x,4)/4;
}
float integral(float a, float b, float(*p)(float)){
return (*p)(b)-(*p)(a);
}
int main(){
float a, b;
float (*p)(float);
printf("请输入下限和上限:");
scanf("%f %f",&a,&b);
p = f2;
printf("%.3f\n",integral(a,b,p));
}
六、返回指针值的函数
I
int * a(int x, int y);
a是函数名,调用以后可以得到一个int*型(指向整型数据)的指针,即整型数据的地址。
括号()优先级高于*,因此a先与括号结合成为一个函数,再返回一个指针。
例题:a个学生各b门课程。要求输入学生序号输出该学生的全部成绩。
#include<stdio.h>
int * search(int (*p)[4], int n){
int * pt = *(p+n); //指向score[n][0]的起始地址
return pt;
}
int main(){
int score[3][4]={
{60,70,80,90},{65,75,85,95},{62,72,82,92}};
int n,*p;
scanf("%d",&n);
printf("The scores of No.%d are:\n",n);
p = search(score,n); //实参是二维数组
for(int i=0;i<4;i++) printf("%d\t",*p++);
printf("\n");
return 0;
}
分析:
search(score,n); 实参score代表数组0行的起始地址即&score[0]
int * search(int (*p)[4], int n){} 形参p的类型是int(*)[4],p指向包含4个整型元素的一维数组。p的基类型是指向一维数组的指针变量。传入实参后,p指向score[0]。这时p是行控制,它的加减都是按照行为单位。p+n表示score[n]的起始地址,*(p+n)表示score[n][0]的起始地址,*(p+n)变成了列控制,它的加减是以单个元素为单位的。int *pt = *(p+n)表示pt存放的是score[n][0]的地址表示pt指向score[n][0]表示pt是列控制。
主函数中p = search(score,n)表示p和自定义函数返回的指针一样指向score[n][0],*p就是score[n][0]的值,*(p+1)就是score[n][1]的值。
思考:如果自定义函数中,变成了int * pt = *p+n;那么返回的指针pt指向什么?指向score[0][n]。主函数中p = search(score,n),p也指向score[0][n],*p就是score[0][n]的值。
例题:对上一题中,找出不及格课程的同学机器学生号。
#include<stdio.h>
int * search(int (*p)[4]){ //p指向score[学生行],列控制
for(int i=0;i<4;i++) if(*(*p+i)<60) return *p; //*p指向score[学生行][0]
return NULL;
}
int main(){
int score[3][4]={
{59,70,80,90},{65,75,80,95},{42,72,82,92}};
int *p;
for(int i=0;i<3;i++)
if((p = search(score+i))==&score[i][0]){ //p指向score[学生行i][0],列控制
printf("No.%d scores:",i);
for(int j=0;j<4;j++) printf("%d ",*p++);
printf("\n");
}
return 0;
}
七、指针数组和多重指针
I指针数组
定义一个指针数组 int * p[4]; // []优先级高于*
使用情况:对于二维数组如果第二维度的长短不一致导致浪费空间,可以使用指针数组。尤其是存放n个长短不一致的字符串,可以使指针数组中的元素分别指向各字符串首字符的地址,p[0]指向字符串"BASIC",p[1]指向字符串"Java"。如果想对他们排序,只需改变指针数组中的各元素的指向(即改变数组元素的值)。
例题:将字符串"BASIC","Java","Great!Wall","Future","Data_Structure"进行排序。
#include<stdio.h>
#include<string.h>
void sort(char * name[],int start, int end){
// for(int i=start;i<end-1;i++) //冒泡排序
// for(int j=start;j<end-i-1;j++)
// if(strcmp(name[j],name[j+1])>0){ //字符串比较函数
// char *temp = name[j];
// name[j] = name[j+1];
// name[j+1] = temp;
// }
for(int i=start;i<end-1;i++){ //简单选择排序
int max=i;
for(int j=i+1;j<end;j++)
if(strcmp(name[max],name[j])>0) max = j;
if(max!=i){
char *temp = name[max];
name[max] = name[i];
name[i] = temp;
}
}
}
void print(char *name[], int n){
// for(int i=0;i<n;i++){
// printf("%s\n",name[i]);
// }
int i=0;
while(i<n) printf("%s\n",*(name+i++));
}
int main(){
char * name[]= {"Java","BASIC","Great!Wall",
"Future","Data_Structure","A!good!"};
sort(name,0,5); //只排序前五个
print(name,6); //输出前六个
return 0;
}
II指向指针数据的指针
定义:char ** p; // * 运算符是右结合
可以这样理解char * (* p)
(*p)表示p是一个指针变量
char * 表示 p指向的是一个 char * 型的数据也就是p指向的是指针型数据
#include<stdio.h>
int main(){
char * name[]= {"Java","BASIC","Great!Wall"};
char **p = name + 2;
//*p就是第三个字符串的起始地址,*p存储的就是一个地址
printf("%d\n",*p); //输出地址的值
printf("%s\n\n",*p); //输出字符串"Great!Wall"
p = name;//说明name也是一个指向指针的指针,相当于一个二维数组。
// name和p都是行控制,加1就是指向下一个元素
for(int i=0;i<3;i++) printf("%s\n",*p++);
printf("\n");
p = name;
for(int i=0;i<4;i++) printf("%c\n",*((*p)++));
//先执行*p,就表示只能活动在第一个字符串内部。
//++表示指针在第一个字符串内部由第一个字符后移
//是不是和指向二维数组的指针很相似
}
例题:有一个指针数组,其元素指针分别指向一个整型数组的某一个元素。输出所有数据。
#include<stdio.h>
int main(){
int a[6] = {0,1,2,3,4,5};
int *num[6] = {&a[5],&a[4],&a[3],&a[2],&a[1],&a[0]};
//指针数组的元素只能存放地址,不能存放整数
int **p;
p=num; //类似于指向二维数组的指针
for(int i=0;i<6;i++) printf("%d\n",*(*p++));
}
III指针数组作main函数的形参
通常main()函数第一行写成 int main(){}
但是在某些情况下,main函数也可以有形参,例如
int main(int argc, char * argv[])
其中,argc( argument count)表示参数个数,argv( argument vector)表示参数向量,它是一个指针数组,数组中每一个元素(指针)指向命令行中的一个字符串。
#include<stdio.h>
int main(int argc, char * argv[]){
// while (argc--)
// printf("%s ",*argv++); //控制台下先编译,再运行 文件名.exe Hello World
//会输出 文件名.exe Hello World
while (argc-->1)
printf("%s ",*++argv); //不会输出文件名,只输出后面的参数
return 0;
}
八、动态分配内存与指向它的指针变量
I
使用malloc函数
函数原型void * malloc ( unsigned int size );
在内存的动态存储区中分配一个长度为size的连续空间。
返回值是分配区域的第一个字节的地址
使用calloc函数
函数原型void * calloc( unsigned n, unsigned size );
在内存的动态存储区中分配n个长度为size的连续空间。一般空间比较大。
calloc函数可以为一维数组开辟动态存储空间。
p = calloc( 50, 4 );
使用free函数
函数原型void free( void * p );
释放指针变量p指向的动态空间。
free( p ); //无返回值
使用realloc函数
函数原型 void * realloc( void * p, unsigned int size );
重新为已经分配了空间的指针分配空间。
realloc(p, 50); //将p所指向的已分配的动态空间改为50字节
头文件全部为 stdlib.h
以前的C版本提供的malloc和calloc函数得到的是指向字符型数据的指针
原型为 char * malloc ( unsigned int size );
如果需要存放整数,要进行类型转换
int * pt = (int *)malloc(100);
C99标准把以上的函数的基类型定义为void类型,称为 无类型指针,不只想哪一种具体的类型数据,只表示用来指向一个抽象的类型的指针,即仅提供一个纯地址而不能指向任何具体的对象
II void指针类型
指向“空类型”的数据,或者指向“不确定类型”的数据。
使用时需要进行类型转换,使之适合于被赋值的变量的类型,
#include<stdio.h>
int main(int argc, char * argv[]){
int a=65;
int * p1 = &a;
char * p2;
void * p3;
p3 = (void *)p1;
p2 = (char *)p3;
printf("%d\n",*p1);
printf("%c\n",*p2);
p3 = &a;
printf("%d\n",*p3);//错误,p3是无指向的
return 0;
}
例题:建立动态数组,输入5个学生的成绩。用自定义函数检查有无不及格的同学,输出不及格成绩。
#include<stdio.h>
#include<stdlib.h>
void check(int *p){
printf("They are fail:\n");
for(int i=0;i<5;i++){
if(*p<60) printf("%d ",*p);
p++;
}
}
int main(){
int * p = (int *)malloc(5*sizeof(int)); //类型转换可以不写,系统自动转换
int n=5;
while(n--) scanf("%d",p++);
check(p-5);
return 0;
}
九、小结
指针就是地址。指针变量是一个存放地址的变量。指针变量的值是一个地址。
指针变量可以为NULL(stdio.h中定义NULL为0)
十、习题
未做,以后复习再做...