C/C++ 语法教程(11)

C/C++ 语法教程(11)

指针(续)

指针进阶

不确定类型的指针——void *

对于一个指针 void *a;,其可以接受任一类型指针的赋值。

比如:

void *a;
int *b;
a=b;

同样其也可以赋值给任一类型的指针,但需要强制类型转换:

void *a=NULL;
int *b;
b=(int *)a;

这里的 NULL 是指空指针,其实际定义指就是 $0$,一般用于没有确切的地址可以赋值时。

同理判断一个指针 $p$ 是否为空指针,也可以使用 if (p)if (!p)

特别地,在给一个 void * 类型指针赋值为确定类型指针后,比如上面那个例子,$a$ 就不能再被赋值为其它类型的指针。虽然编译器无问题,但其实际值将不可预测

也就是说,实际上 $a$ 的类型已经不再是不确定了。

动态内存的申请和释放

对于一些数组,其大小较大,但是使用范围较小。

让其在整个程序中都占用内存不完全合理。

所以会有在需要时创建,不需要时销毁的想法。

这也就是动态内存的申请和释放。

下面介绍一下这个过程中常用的函数(一般需要 #include <cstdlib> 或者 #include <stdlib.h> 或者 #include <malloc.h>):

  1. malloc() 函数

    malloc() 函数用于从内存中申请指定字节数的内存,并返回内存块的首地址(指针类型)。

    函数原型为:

    void *malloc(size_t size);
    

    可以发现返回值是不确定类型指针,所以实际调用时必须进行强制类型转换。

    举一些例子:(参见 THUEE 版教程)

    #include <cstdlib>
    using namespace std;
    int main()
    {
       char *p;
       double **q;
       int (*a)[4];
       int (*b)[10][10],m;
       int *c,n;
       scanf("%d",&n);
       scanf("%d",&m);
       p=(char *)malloc(sizeof(char)*20);
       q=(double **)malloc(sizeof(double *)*n);
       for (int i=0;i<n;i++)
           q[i]=(double *)malloc(sizeof(double)*m);
       a=(int (*)[4])malloc(sizeof(int)*5*4);//a[5][4]
       b=(int (*)[10][10])malloc(sizeof(int)*10*10*m);//b[m][10][10]
       c=(int *)malloc(sizeof(int)*n);
       //do something...
       free(c);
       free(b);
       free(a);
       free(q);
       free(p);
       return 0;
    }
    
    

    free 函数后面讲)

    这里示范了一维及多维数组的集中申请内存方法,相信大家看着例子就能有所理解。

    当然,也有可能申请内存失败,那么就会返回空指针(NULL,所以使用前最好判断是否获取成功(一般问题不大)。

  2. calloc() 函数

    函数原型为:

    void *calloc(size_t n,size_t size);
    

    含义是申请 $n$ 个长度为 $size$ 的连续空间,并返回指向分配起始地址的指针。

    同样申请不成功,会返回空指针(NULL

    与前面 malloc 不同的是,calloc 会将申请的内存空间自动初始化为 $0$,而 malloc 申请的内存空间则会是原来的不知道是啥的随机数据。

    具体使用方法类似,只举一个例子:

    char *p;
    p=(double *)calloc(20,sizeof(char));
    
  3. realloc() 函数

    函数原型为:

    void *realloc(void *mem_address,size_t newsize);
    

    其一般使用方法是:

    指针名=(数据类型 *)realloc(指针名,新的内存长度)

    其作用是,在原来已经申请内存块 mem_address 的基础上,再重新申请一块更长或者更短的内存块(更长则新分配部分不初始化,更短则原来的后面的数据可能丢失)。实际使用过程中,如果可以直接向后扩张,则直接扩张,并返回原地址 mem_address,否则将重新分配一块 newsize 大小的内存空间,然后将原来的 mem_address 所指向的内存块完整复制给新的内存块,同时返回新申请的的内存空间首地址。

    同样,如果申请失败,返回空指针(NULL

    具体可以看下下面的例子:

    #include <cstdlib>
    using namespace std;
    int main()
    {
       int *pn=(int *)malloc(5*sizeof(int));
       //do something...
       pn=(int *)realloc(pn,10*sizeof(int));
       //do something...
       free(pn);
       return 0;
    }
    
    
  4. free() 函数

    函数原型为:

    void free(void *ptr);
    

    无论用上面哪个函数申请的动态内存块,都应该用 free 函数重新释放回内存堆。

    而且建议释放顺序与利用上面三个函数申请时的顺序相反,防止产生内存碎块。

    额外注意的几点问题:

    1. 如果没有 free 掉申请的动态内存块,这些局部指针变量所指的动态内存块在函数执行完毕后也不会自动释放。
    2. free(p) 对于 p==NULL 执行多少次都不会有问题,但对于 p!=NULL 则在连续两次后会产生错误。所以一般在 free(p) 后会立即执行 p=NULL,防止出错。
    3. free 时传入指针必须为申请时的首地址,所以如果进行过 p++ 这类操作后,应该记得还原为首地址,再进行 free(p) 操作。

字符串与指针

一般有两种方式表示字符串,即 char s[]="Hello World!";char *s="Hello World!";

char 型指针跟 char 型数组十分相似,比如获取第 $i$ 个字符都可以用 s[i]

但是注意下列区别:

//1
char s[20];
s="Hello World!";

s=s+6;
printf("%s\n",s);

//2
char *s;
s="Hello World!";

s=s+6;
printf("%s\n",s);
  1. 其中第一种都是不合法的,而第二种都是合法的,输出是 World!

  2. 除此之外,对于 char * 类型,只能整体修改整个字符串,不能修改其中具体的某个字符,因为这个字符串被认为是一个常量。

  3. 同理也不能对 char * 进行直接读入操作。

  4. 另外,我们知道,在输入输出函数中,第一个参数是一个格式控制字符串。而这个字符串也可由 char * 给出。

    比如:

    int a,b;
    //do something...
    char *format="%d,%d\n";
    printf(format,a,b);
    
  5. char * 数组可以构造紧凑型字符串数组。如:
    char *str[]={"A","BC","DEF","GHIJK"};
    

    这时每个数组元素指针指向一个字符串,所以可以使用 str[1]=str[2],但不能使用 strcpy(str[1],str[2]),因为字符型指针所指的字符串都是常量,无法修改。

    但如果使用二维 char 数组:

    char str[][6]={"A","BC","DEF","GHIJK"};
    

    则可以使用 strcpy(str[1],str[2]) 但无法使用 str[1]=str[2],因为这些指向数组元素的指针类型是常量。

strstr 函数

原型是:char *strstr(char *str1,char *str2);

其功能是在 $str1$ 中查找字符串 $str2$ 是否出现过,是则返回第一个出现的位置的地址(指针),否则返回 NULL

函数指针

指向函数指针变量的定义形式:

类型 (*指针变量名)(所指函数的参数类型说明列表);

比如:

int test(int a)
{
    return a;
}
int main(int argc, const char * argv[])
{
    int (*fp)(int a);
    fp = test;
    cout<<fp(2)<<endl;
    return 0;
}

这样调用就是可以的。

甚至我们可以用 typedef 来简化定义。

int test(int a)
{
    return a;
}
int main(int argc, const char * argv[])
{
    typedef int (*fp)(int a);
    fp f = test;
    cout<<f(2)<<endl;
    return 0;
}

函数指针同样是可以作为参数传递给函数的。

int test(int a)
{
    return a-1;
}
int test2(int (*fun)(int),int b)
{
    int c = fun(10)+b;
    return c;
}
int main(int argc, const char * argv[])
{
    typedef int (*fp)(int a);
    fp f = test;
    cout<<test2(f, 1)<<endl; // 调用 test2 的时候,把test函数的地址作为参数传递给了 test2
    return 0;
}

利用函数指针,我们可以构成函数指针数组,更明确点的说法是构成指向函数的指针数组。

void t1(){cout<<"test1"<<endl;}
void t2(){cout<<"test2"<<endl;}
void t3(){cout<<"test3"<<endl;}

int main(int argc, const char * argv[])
{
    typedef void (*fp)(void);
    fp b[] = {t1,t2,t3}; // b[] 为一个指向函数的指针数组
    b[0](); // 利用指向函数的指针数组进行下标操作就可以进行函数的间接调用了

    return 0;
}

main 函数的形参

实际上 main 函数并不是一定没有形式参数,如果运行程序的时候外界为其传递参数,则就需要带形参的 main 函数。

一般写如下形式:

int main(int argc,char *argv[])
{
    //do something...
}

一般在命令行中运行一个 name 的可执行程序(name.exe 等),一般就是输入 name 并回车。

而如果想给这个 name 传参数,一般会输入 name str1 str2 ... strn 并回车,其中后面是 $n$ 个字符串。

这样(假设 name 是由 C/C++ 文件编译而来)就相当于给 main 函数传递了 $n + 1$ 个参数。(包括 name 本身)

其中 argc=n+1argv 存储着这 $n + 1$ 个字符串。

比如:

#include <cstdio>
using namespace std;
int main(int argc,char *argv[])
{
    for (int i=0;i<argc;i++)
        printf("%s\n",argv[i]);
    return 0;
}

假设可执行文件名为 name,则在对应目录下的命令行中输入 name Hello World 233 并回车,程序就会输出:

name
Hello
World
233

总结

这一章对于指针的用法有了更多的深入了解,当然实际上大多数的作用还很不明显,主要要记住字符串与指针的关系。

函数指针也有一定用处,可以等到与后来的函数对象进行对比。

因为题目比较难以体现用处,所以同样不布置习题。

 

点赞 0

No Comments

Add your comment