再学习c指针-int型指针及其他


#C 语言#


2014-07-24

这是一年前的笔记。

环境:mint 13 64bit,gcc 4.6.3 如无特殊,均为gcc test.c -o test的类似方式编译源文件。

相关基础:汇编,编译原理

在这里主要讨论int*,int**,char*,char**等这些比较基本的指针类型,主要以int类型为示例。在64bit环境中地址用8B表示,int类型是4B。

直接寻址、间接寻址

在汇编语言的学习中,寻址方式对于了解指针很重要。

关于int *

int *p定以了一个整型指针变量p,p本身存放的是一个int变量的地址,即在32位机器上int变量为2Bytes,p实际为4Bytes,因为地址要用32bits表示。 若有以下代码:

#include <stdio.h>
int main()
{
    int *p;
    printf("%p\n",p);
    printf("%d\n",*p);
    return 1;                       
}

编译时会提示:warning: ‘p’ is used uninitialized in this function [-Wuninitialized] 运行时显示:

(nil)
Segmentation fault (core dumped)

这是因为声明p时候,分配的8个字节原先的内容并没有被抹去,故p指向的内容不定,甚至指向内核,这是OS不允许的。(现在觉得p的值每个bit应该为0更合适一些,还是主要看编译器如何去实现)。

下面这段代码就可以正常运行:

#include <stdio.h>
int main()
{
    int i;  //默认初始化为0
    int *p=&i;
    printf("%p\n",&i);  //打印int变量i的地址
    printf("%p\n",&p);  //打印int指针变量p的地址
    printf("%p\n",p);   //打印p本身的内容
    printf("%d\n",*p);  //打印p本地代表的内存地址指向的值,即i的值。
    return 1;                       
}

运行结果为:(注意内存对齐的概念)

0x7fffcbaecf5c
0x7fffcbaecf50
0x7fffcbaecf5c
0

内存组织如下:从内存0x7fffcbaecf5c处开始4个Byte存放int变量i,从0x7fffcbaecf50开始的8个Byte开始int指针变量p的值,而这个值对应的内存单元在使用*p取值时只会取4个Byte。调用i时候类似直接寻址,调用*p时候类似间接寻址。因为p是int型指针。注意int i;int*p=&i;int i;int *p;p=&i;是等价的,从这个角度来讲,可以把int *单独看作一种类型。虽然int i;(int *)p=&i;是错误的,但是比较容易理解(至少对我而言),例如在经典的交换数值问题上:

#include <stdio.h>
void swap(int *p1,int *p2);
int main()
{
    int i=0,j=1;
    swap(&i,&j);
    printf("i=%d,j=%d\n",i,j);
    return 1;                       
}
void swap(int *p1,int *p2)
{
    int temp;
    temp=*p2;
    *p2=*p1;
    *p1=temp;    
}

对于swap函数的参数int *p1,int *p2,只看p1、p2,两者是int型指针,本身代表地址。 所以调用的时候需要把地址填入里面,也就是&i、&j,注意i和j必须是int类型。 在上面的swap代码修改如下:

#include <stdio.h>
void swap(int *p1,int *p2);
int main()
{
    int i=0,j=1;
    printf("i addr:%p ,value:%d ; j addr :%p ,value:%d \n",&i,i,&j,j);
    swap(&i,&j);
    printf("i addr:%p ,value:%d ; j addr :%p ,value:%d \n",&i,i,&j,j);
    return 1;                       
}
void swap(int *p1,int *p2)
{
    printf("p1 value:%p , p2 value:%p ; *p1 value :%d , *p2 value:%d \n",p1,p2,*p1,*p2);
    int temp;
    temp=*p2;
    *p2=*p1;
    *p1=temp;   
    printf("p1 value:%p , p2 value:%p ; *p1 value :%d , *p2 value:%d \n",p1,p2,*p1,*p2);
}

输出可能如下:

i addr:0x7fff6ce4b608 ,value:0 ; j addr :0x7fff6ce4b60c ,value:1 
p1 value:0x7fff6ce4b608 , p2 value:0x7fff6ce4b60c ; *p1 value :0 , *p2 value:1 
p1 value:0x7fff6ce4b608 , p2 value:0x7fff6ce4b60c ; *p1 value :1 , *p2 value:0 
i addr:0x7fff6ce4b608 ,value:1 ; j addr :0x7fff6ce4b60c ,value:0 

可以看出,在main()中调用swap()时候传递的是地址。如果是传值方式,则在swap中打印的地址肯定会不同,见下:

#include <stdio.h>
void swap(int p1,int p2);
int main()
{
    int i=0,j=1;
    printf("i addr:%p ,value:%d ; j addr :%p ,value:%d \n",&i,i,&j,j);
    swap(i,j);
    printf("i addr:%p ,value:%d ; j addr :%p ,value:%d \n",&i,i,&j,j);
    return 1;                       
}
void swap(int p1,int p2)
{
    printf("p1 addr:%p , p1 value:%d ; p2 addr :%p , p2 value:%d \n",&p1,p1,&p2,p2);
    int temp;
    temp=p2;
    p2=p1;
    p1=temp;   
    printf("p1 addr:%p , p1 value:%d ; p2 addr :%p , p2 value:%d \n",&p1,p1,&p2,p2);
}

运行结果可能如下:

i addr:0x7fffef6b9f98 ,value:0 ; j addr :0x7fffef6b9f9c ,value:1 
p1 addr:0x7fffef6b9f6c , p1 value:0 ; p2 addr :0x7fffef6b9f68 , p2 value:1 
p1 addr:0x7fffef6b9f6c , p1 value:1 ; p2 addr :0x7fffef6b9f68 , p2 value:0 
i addr:0x7fffef6b9f98 ,value:0 ; j addr :0x7fffef6b9f9c ,value:1 

既然这样,我们可以利用int *使函数“返回”多个数值。如下:

#include <stdio.h>
void fun(int *p1,int *p2);
int main(int argc, const char *argv[])
{
    int m=0,n=1;
    printf("m=%d\tn=%d\n",m,n);
    fun(&m,&n);
    printf("m=%d\tn=%d\n",m,n);
}
void fun(int *p1,int *p2)
{
    *p1=*p1+1;
    *p2=*p2+1;
}

使用指针时候应该注意防止发生下面两种情况:
第一种:

int *p;
*p=20;

这个可以看成错误,在gcc下编译是类似warning: ‘p’ is used uninitialized in this function [-Wuninitialized]这样的警告。运行程序时候会出现这样的结果:Segmentation fault (core dumped)。因为指针p只是被定义了,但是并没有指向具体的内存单元。

第二种:

float f=1.2;
int *p;
p=&f;

这个在编译时候也只是出现警告,运行结果为Segmentation fault (core dumped)。p指向的应该是int型变量,而非float——至少int和float变量所占内存就不一样。

int *与一维int数组

在应用中,int *不只是可以用来指向一个int变量,也可以指向一个int数组——虽然本质是一样的。下面是示例:

#include <stdio.h>
int main(int argc, const char *argv[])
{
    int a[3]={1,2,3};
    int *p;
    p=a;
    printf("1---%d\t%d\t%d\n",a[0],a[1],a[2]);
    printf("2---%d\t%d\t%d\n",p[0],p[1],p[2]);
    printf("3---%d\t%d\t%d\n",*a,*(a+1),*(a+2));
    printf("4---%d\t%d\t%d\n",*p,*(p+1),*(p+2));
    printf("4---%d\t%d\t%d\n",*p,*(p+1),*(p+2));
    printf("5---%d\t%d\t%d\n",*p,*p++,*p++);  //++比*优先级高,所以相当于*(p),*(p++),*(p++)
    p=a;
    printf("6---%p\t%p\t%p\n",a,a+1,a+2);
    printf("7---%p\t%p\t%p\n",p,p+1,p+2);    
    printf("%d\n",a[3]);
    return 0;
}

运行结果如下:

1---1   2   3
2---1   2   3
3---1   2   3
4---1   2   3
4---1   2   3
5---3   2   1
6---0x7fff74e47a20  0x7fff74e47a24  0x7fff74e47a28
7---0x7fff74e47a20  0x7fff74e47a24  0x7fff74e47a28
0

对于数组a来说,a代表的是数组a的首地址,*a则是首地址对应的元素a[0]。a+1代表a首地址再偏移一个int大小后的地址——即a[1]的地址,所以*(a+1)代表的是a[1]。为什么是偏移int大小呢,因为数组是int类型,所以这种类似*(a+1)的调用同样适用于float、char、struct数组。
printf("5---%d\t%d\t%d\n",*p,*p++,*p++);的输出是3 2 1是因为printf的可变参数列表是从右向左读取的。 在printf("%d\n",a[3]);中a[3]已经越界,但是程序依然可以运行。

int *与malloc

首先man一下malloc:

zsh >> man 3 malloc | cat
MALLOC(3)                  Linux Programmer's Manual                 MALLOC(3)
NAME
       malloc, free, calloc, realloc - Allocate and free dynamic memory
SYNOPSIS
       #include <stdlib.h>
       void *malloc(size_t size);
       void free(void *ptr);
       void *calloc(size_t nmemb, size_t size);
       void *realloc(void *ptr, size_t size);
DESCRIPTION
       The malloc() function allocates size bytes and returns a pointer to the
       allocated memory.  The memory is not initialized.  If size is  0,  then
       malloc()  returns either NULL, or a unique pointer value that can later
       be successfully passed to free().
...省略部分内容...
RETURN VALUE
       The malloc() and calloc() functions return a pointer to  the  allocated
       memory  that  is  suitably aligned for any kind of variable.  On error,
       these functions return NULL.  NULL may also be returned by a successful
       call  to  malloc() with a size of zero, or by a successful call to cal‐
       loc() with nmemb or size equal to zero.
...省略部分内容...

malloc函数的原型是void *malloc(size_t size);,void* 表示未确定类型的指针,void *可以指向任何类型的数据,在使用该函数时候应该明确指定这个新申请的空间保存什么数据类型。例如(int*)malloc(8)表示申请的空间保存两个int数(int为4B),更直观的写法应该是(int *)malloc(2*sizeof(int))。很显然,把(double *)malloc(2*sizeof(double))的地址提供给一个int指针是不合适的。如下:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
    int *p;
    double *p1;
    p=(double*)malloc(2*sizeof(double));
    p[0]=1.2;
    p[1]=2;
    printf("%d\t%d\n",p[0],p[1]);
    printf("%ld\n",sizeof(p[0]));
    p1=p;
    printf("%ld\n",sizeof(p1[0]));
    printf("%lf\n",p1[0]);
    return 0;
}

编译时候会有下面的警告:

7:6: warning: assignment from incompatible pointer type [enabled by default]
12:7: warning: assignment from incompatible pointer type [enabled by default]

(double*)malloc(1)这样,也会引发警告。

有一点需要注意,malloc的空间是分配出来了,但是对应内存中的内容与原先不变,即这些内存空间未被初始化。

int *与NULL

NULL是一个宏定义,用来表示空指针常量。用哪个具体的地址值表示空指针取决于系统的实现。通过下面这个简单的示例可以更好的理解NULL:

#include <stdio.h>
int main(int argc, const char *argv[])
{
    int i=1;
    int *p1=&i;
    int *p2;
    int *p3=NULL;
    if(&i==NULL)
    {
        printf("&i is NULL\n");
    }
    if(p1==NULL)
    {
        printf("p1 is NULL\n");
    }
    if(p2==NULL)
    {
        printf("p2 is NULL\n");
    }
    if(p3==NULL)
    {
        printf("p3 is NULL\n");
    }
    return 0;
}

编译时候会对if(&i==NULL)作出警告:warning: the comparison will always evaluate as ‘false’ for the address of ‘i’ will never be NULL [-Waddress]。同时也会对if(p2==NULL)作出警告:warning: ‘p2’ is used uninitialized in this function [-Wuninitialized]。运行结果如下:

p3 is NULL

如果程序中需要对一些指针进行NULL判断,那么int *p3=NULL;这种写法是一个好习惯。

int *与二维int数组

二维数组默认是行优先存储,且存放在连续的内存中。示例如下:

{% raw %}
#include <stdio.h>
int main(int argc, const char *argv[])
{
    int a[2][3]={{1,2,3},{4,5,6}};
    printf("1--%d\n",a[1][2]);
    printf("2--%d\t%d\t%d\t%d\n",a[1][2],*(*(a+1)+2),*(a[1]+2),*(&a[1][2]));
    printf("3--%p\t%p\t%p\n",&a[1][2],*(a+1)+2,a[1]+2);
    printf("4--%p\t%p\t%p\t%p\n",a,*a,a[0],*(a+0));
    printf("5--%p\t%d\t%d\t%d\n",*a,*(*a),*(a[0]),*(*(a+0)));
    return 0;
}
{% endraw %}

运行结果如下:

1--6
2--6    6   6   6
3--0x7fff8ef28404   0x7fff8ef28404  0x7fff8ef28404
4--0x7fff8ef283f0   0x7fff8ef283f0  0x7fff8ef283f0  0x7fff8ef283f0
5--0x7fff8ef283f0   1   1   1

a本身代表的是二维数组a的首地址,*a代表的是a的第一行的首地址。这也就意味着a虽然和a[0][0]地址相同,但*a并不是a[0][0]。也就是说,要获取二维数组某一元素,必须通过两次取地址,类似地适用于更高维的数组,至于原因,肯定实在代码区。

int *与一维维数组的差别不在为数据分配的内存上(当然,两者分配的地方不同,一个堆,一个栈),而在代码段。 不能想当然的认为int**与二维数组区别也不大,若要使用int**做一个“二维数组”还是比较麻烦的。

int *不能比较正规的表示表示多维数组,如下:

{% raw %}
#include <stdio.h>
int main(int argc, const char *argv[])
{
    int a[2][3]={{1,2,3},{4,5,6}};
    int i=1,j=2;
    int *p=a;
    printf("%d\n",a[i][j]);
    printf("%d",*(p+i*3+j));
}
{% endraw %}

编译时候会有类型不一致的警告,运行结果如下:

6
6

int **模拟二维数组

先看一下sizeof()一个指针是什么结果:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
    int *p=(int *)malloc(10*sizeof(int));
    printf("%d\n",sizeof(int *));
    printf("%d\n",sizeof(p));
    printf("%d\n",sizeof(*p));
    return 0;
}

运行结果如下:

8
8
4

无论指针指向多大的内存空间,sizeof()永远是一个存放内存地址所需空间的大小。

再看一个int**的代码:

#include <stdio.h>
int main(int argc, const char *argv[])
{
    int m=10;
    int *p;
    int **p1;
    p=&m;
    p1=&p;
    printf("%d\n",m);
    printf("%d\n",*p);
    printf("%d\n",**p1);
    return 0;
}

很明显,运行结果为:

10
10
10

下面模拟一下二维数组:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
    int **p;
    p=(int **)malloc(2*sizeof(int *));
    p[0]=(int *)malloc(10*sizeof(int));
    p[1]=(int *)malloc(10*sizeof(int));
    printf("%ld\n",sizeof(p[0]));
    printf("%ld\n",sizeof(p[0][1]));
    p[0][0]=100;
    p[0][1]=101;
    printf("%d,%d\n",p[0][0],p[0][1]);
    //内存地址
    printf("%p,%p\n",&p[0],&p[1]);
    printf("%p,%p\n",p[0],p[1]);
    return 0;
}

运行结果如下:

8
4
100,101
0x2128010,0x2128018
0x2128030,0x2128060

话说模拟完之后,真感觉应该把“模拟”二字去掉,但是这个还是和int a[2][10]不同。 对于int a[2][10],例如取元素a[1][2],获取a[1][2]地址的过程应该是a首地址 + 1*10*sizeof(int) + 2*sizeof(int)。(假定数组元素由低地址向高地址方向存放)。
对于上面代码中的自定义二维数组p,取元素p[1][2]过程应该是在将获取的p对应的那个地址偏移8B(即1*sizeof(int*))的后得到新的地址,在新地址取得8B的数据作为新地址,跳到新地址后,便宜8B(即2*sizeof(int)),如此便获得元素p[1][2]。

malloc

要使得程序健壮,malloc的内存空间在不使用后一定要用free释放,这同样适用于非main函数。这其中还包含内存泄漏的问题。 如果程序是这样:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
    int *p;
    p=(int *)malloc(2*sizeof(int));
    printf("%p\n",p);
    p=(int *)malloc(2*sizeof(int));
    printf("%p\n",p);
    return 0;
}

很显然,第一次malloc申请的内存是想释放也释放不了,除非程序结束,OS回收内存。

int *与const

用关键字const修饰一个符号后,该符号不能被赋值,但是这并不代表这个符号变成了常量。const可以理解为read-only。

对于const int m=10;
const修饰m,代表之后不能再给m赋值,例如m=0就会报错。const int m=10;int const m=10;同义。

对于int i=0;const int *m=&i;
const修饰*m,这也就代表着之后执行*m=10;是错误的。不过改变m自身的内容倒是没有什么问题,例如int i=0,j=1;const int * p = &i;p=&j;const int *m=&iint const *m=&i同义。

对于int i=0,j=1;int * const p = &i;*p=7;: const修饰p,代表着p=&j是错误的。

对于int i=0,j=1;const int * const p=&i;
在这种情况下,*p=20p=&j都是错误的。

int *p[4]int (*p)[4]

对于int *p[4][]的优先级高于取值运算符*,所以可以看作int *修饰数组p的四个元素,即四个元素本身都是一个int指针。请参考下面的程序:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
    int *p[4];
    p[0]=(int *)malloc(2*sizeof(int));
    p[1]=(int *)malloc(2*sizeof(int));
    p[2]=(int *)malloc(2*sizeof(int));
    p[3]=(int *)malloc(2*sizeof(int));
    p[0][0]=10;
    printf("%d\n",p[0][0]);
    return 0;
}

对于int (*p)[4]: 这声明了一个指向数组的指针,数组内有4个int元素。注意int (*p)[4]只是作了声明,并未初始化。示例如下:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
    int (*p)[4];
    p=(int (*)[4])malloc(4*sizeof(int));
    (*p)[0]=1;
    printf("%d\n",(*p)[0]);
    return 0;
}

其中p=(int (*)[4])malloc(4*sizeof(int));对p做了初始化。如果是p=(int *)malloc(4*sizeof(int));则会有类型不兼容的警告。

int *func()(int *)func()int (*func)()

对于int *func(): 这个函数应该返回一个指针,例如:

#include <stdio.h>
#include <stdlib.h>
int *func();
int main(int argc, const char *argv[])
{
    int *p=func();
    printf("%d,%d\n",p[0],p[1]);
    free(p);
    return 0;
}
int *func()
{
    int *p=(int *)malloc(2*sizeof(int));
    p[0]=1;p[1]=2;
    return p;
}

对于(int *)func()
()的优先级比*高,自己的理解是这种声明在意思上和int *func()相同,不过这是错误的,请遵守规则.

对于int (*func)()点击这里

char *

float、double类型的指针和int完全一样,倒是char型指针有一些不一样的地方。
char指针可以这样声明:char *p="qweasd";,而int指针不可以这样int *p=123;
另外一维的char数组认为是一个字符串,可以使用printf("%s",p);直接输出数组内荣,直到遇到\0

关于地址的一件有意思的事情

看下面这个程序:

#include <stdio.h>
int main()
{
    int i=0;
    int a[2];
    int b[2][3];
    printf("%p\n",&i);
    printf("%p\n",&a[1]);
    printf("%p,%p\n",b[1],&b[1]);
    return 0;
}

运行结果是:

0x7fffa0ac4f2c
0x7fffa0ac4f24
0x7fffa0ac4f0c,0x7fffa0ac4f0c

对于二维数组b,b[1]和&b[1]都是输出b第一行的首地址,但是&&b[1]就会报错,还未理解。

准备找个时间看看Kenneth A.Reek的《C和指针》这本书。


( 本文完 )