[C] C指针基础概览

C指针概述

在C语言中,指针不仅可以表示变量的地址,而且还可以存储数组,数组元素,函数,文件设备的地址,C指针的主要特征具体如下:

  • 通过一个变量声明时在前面使用“*号”,表面这是个指针型变量。该变量存储了一个内存地址。
  • 单目运算符*(不是指代表乘法的运算符)是获取指向内容的操作符,用来获取内存地址里存储的内容。
  • 弹幕运算符 & 是获取地址的操作符,用来获取变量的地址。

该文章里所有的测试均是在64位Windows环境下的Visual Studio 2019 中进行。

指向标量的C指针

标量是指仅含有单个值的变量,比如整型(int),长整型(long),浮点型(float)等普通类型以及指针类型的变量。

指向地址的指针

如下的例子,它定义了一个int型的证书,然后定义了两个指针,一个是myp,另一个是mypp。myp和mypp都是指针变量,但指向的内容是不同的,myp指向x的地址,mypp指向myp的地址,通过myp可以找到x,而通过mypp则不能立即找到x,mypp先找到myp,然后再通过myx找到x,因此,mypp也称为指针的指针,也称指向地址的指针。

#include <stdio.h>

int main(void)
{
    int x;
    x = 128;
    int* myp = &x;
    int** mypp = &myp;
    printf("           x:%d\n", x);
    printf("         myp:%p\n", myp);
    printf("        mypp:%p\n", mypp);
    printf("mypp address:%p\n", &mypp);

    return 0;
}

在visual studio中得到的结果为

           x:128
         myp:00F3FDC4
        mypp:00F3FDB8
mypp address:00F3FDAC
取地址与解引用操作符

在C语言中可以通过取地址操作符 & 获取变量的地址,解引用操作符 * 用于提取指针指向的内容。

#include <stdio.h>

int main(void)
{
    int x;
    x = 128;
    int* myp = &x;
    int** mypp = &myp;
    printf("           x:%d\n", x);
    printf("         myp:%p\n", myp);
    printf("        mypp:%p\n", mypp);
    printf("mypp address:%p\n", &mypp);
    printf("        *myp:%d\n", *myp);
    printf("      **mypp:%d\n", **mypp);

    return 0;
}

在Visual Studio 2019中,可以得到以下的运行结果

           x:128
         myp:012FFC5C
        mypp:012FFC50
mypp address:012FFC44
        *myp:128
      **mypp:128

其中,*mypp通过一次解引用,访问mypp指示的地址,获取该地址的内容(内容为指针型),mypp指向了myp的地址,myp指向了x所在的内存地址,因此,对mypp进行一次解引用操作就可以获得myp变量的值(即x的内存地址)。

**mypp拥有两个解引用符,第一个解引用符去除mypp中存储的myp的地址,第二个解引用符取出myp中存放的x值,对mypp的二次解引用操作会将变量x的内存取出,并使用参数“%d”指定了该内容的大小为int型,然后使用printf函数输出到屏幕上。

指针与指向内容的大小
指针本身的大小

指针本身的大小可有寻址的字长来决定,以32位的CPU为例。每个地址均可以用32位数值进行编码,也就是说用32位数值就可以代表程序中需要引用的任何地址。在计算机中,字是用来一次性处理事务的一个固定长度的位组(也可以称为比特组),通常,字长为8位数据的CPU称为8位CPU,字长为32位数据的CPU称为32位CPU,目前市面上主流的CPU绝大部分都达到了64位。

C语言使用指针存储地址,假设在32位CPU中使用32位编译器进行编译,那么指针的大小就是32位,这样32位CPU的最大寻址空间大小就是2的32次方,也就是4GB左右(这里所说的内存地址均是指虚拟内存地址)

指针指向内容的大小

既然在32位CPU的PC中,每个指针均只有32位大小,那么C语言编译器如何知道这个指针所指向内容的大小呢?其奥秘在于,声明一个指针,需要指定它指向的数据类型。C语言声明指针的格式通常为 “指向数据的类型* 变量名”。

在32位CPU中,数据类型的大小通常如下

数据类型大小(字节)
char1
short int2
int4
unsigned int4
float4
double4
long4
long long8
unsigned long4
指针类型4

在64位CPU中,数据类型的大小通常如下

数据类型大小(字节)
char1
short int2
int4
unsigned int4
float4
double8
long4
long long8
unsigned long8
指针类型8

现代操作系统都有自己的虚拟内存系统,这使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,通常是在需要时再进行数据交换,因此,在物理内存只有2GB的PC里,通过虚拟内存机制,内存可以扩展到4GB。

指向数组的C指针

C语言的数组属于非标量的符合类型,数组中可存放多个数组元素,每个数组元素可以是基本数据类型或者复合类型,根据数组元素的类型不同,数组又可分为

  • 数值数组
  • 字符数组
  • 指针数组
  • 结构数组

等。数组有以下的特征

  • 数组的元素都具有相同的数据类型
  • 数组元素使用同一个名字,但使用不同的编号,这个名字称为数组变量名,编号为索引或下标(从0开始)
  • 数组的每个元素都在内存中有对应的地址,且这些地址都可以通过指针进行存储。访问方式为 “ 数组名[索引] ”,&数组名[索引] 则可以获得第索引个元素的地址。
#include <stdio.h>

int main(void)
{
    int i;
    char x[20] = "0123456789ABCDEFGHIJ";

    for (i = 0; i < 20; i++)
    {
        printf("x[%d]:%c\n", i, x[i]);
    }

    char* p_x;
    for (p_x = &x[0]; p_x < &x[20]; p_x++)
    {
        printf("%c", *p_x);
    }
    printf("\n");
    return 0;
}

在Visual Studio 2019上执行以上程序可以得到如下结果

x[0]:0
x[1]:1
x[2]:2
x[3]:3
x[4]:4
x[5]:5
x[6]:6
x[7]:7
x[8]:8
x[9]:9
x[10]:A
x[11]:B
x[12]:C
x[13]:D
x[14]:E
x[15]:F
x[16]:G
x[17]:H
x[18]:I
x[19]:J
0123456789ABCDEFGHIJ

指针数组

指针数组

所谓指针数组就是数组的元素类型为指针,每个元素存放着一个内存地址。

#include <stdio.h>

int main(void)
{
    int i;
    char x[10] = "ABCDEFGHIJ";
    char* p_x[10]; // 定义一个指针数组,数组的内容是10个指针类型
    
    for (i = 0; i < 10; i++)
    {
        p_x[i] = &x[i];  // 将数组中的每一个元素的地址依次赋给p_x的每一个元素
    }
    for (i = 0; i < 10; i++)
    {
        printf("%c ", *p_x[i]);  // 以数组下标的方式访问每一个数组的地址,然后对这些元素解引用后输出原数组的内容
    }
    return 0;
}

在Visual Studio 2019中得到的结果为

A B C D E F G H I J
指向指针数组的指针

指针指向指针数组的含义为:指针数组的所有元素都是指针,有某个指针指向这个指针数组的某个元素的地址。

#include <stdio.h>

int main(void)
{
    int i;
    char x[10] = "ABCDEFGHIJ";
    char* p_x[10];

    for (i = 0; i < 10; i++)
    {
        p_x[i] = x + i;  // p_x是一个指针数组,里面包含了数组x中每一个元素的地址
    }
    
    char** pp_x = NULL;
    for (i = 0; i < 10; i++)
    {
        printf("%c ", *p_x[i]);  
    }
    printf("\n");
    for (pp_x = p_x; pp_x < (p_x + 10); pp_x++)  // pp_x最初指向p_x的起始地址,通过++操作,依次指向p_x的下一个元素
    {
        printf("%c   ", **pp_x);  // 通过两次解引用符将最初的元素(x数组里的值)输出来
    }
    return 0;
}

在Visual Studio 2019中运行的结果为

A B C D E F G H I J
A   B   C   D   E   F   G   H   I   J
指向了指向指针数组的指针的指针

名字听起来比较绕口,但是并不难理解,就是某个指针指向了指向的地址里存放了另一个指向指针的指针。

#include <stdio.h>

int main(void)
{
    int i;
    char x[10] = "ABCDEFGHIJ";
    char** pp_x[5];
    char* p_x[10];

    for (i = 0; i < 10; i++)
    {
        p_x[i] = x + i;
    }
    char*** temp_x = pp_x;
    for (i = 0; i < 10; i += 2)
    {
        *temp_x = &p_x[i];
        temp_x++;
    }

    printf("\n");

    char*** ppp_x;
    for (ppp_x = pp_x; ppp_x < (pp_x + 5); ppp_x++)
    {
        printf("%c  ", ***ppp_x);
    }
    return 0;
}

在Visual Studio 2019中运行的结果为

A  C  E  G  I
多维数组指针

多维指针数组的指针比一维指针数组更灵活,因为它可以指定指向变量的最后一维的维数。

#include <stdio.h>

int main(void)
{
    int i;
    int x[2][5] = { 1,2,3,4,5,6,7,8,9,10 };

    // 这是数组指针,即指向一个长度为5个元素数组的指针,也称为行指针
    int(*p_x)[5];  // 每次该指针加1,相当与跳过5个整型变量。
    for (p_x = x; p_x <= (&x[1]); p_x++)
    {
        printf("%d  ", *p_x[0]);
    }
    return 0;
}

上述程序,定义指针p_x时,指定它指向的内容是5个int整型数,每移动一次p_x指针,都将跳过5个整数。,使用“%d”输出时,程序仅输出每一维的第一个元素,因为“%d”作为printf的参数,仅输出一个32位大小的整数。
在Visual Studio 2019中运行的结果为

1 6
对多维指针数组的灵活定义

实际上,定义了指向多维指针数组后,可以以任意的形式来访问数组中的元素,如下程序,定义的指向多维数组的指针为指向两个整数的多维指针数组,那么每次p_x加1,则跳过2个元素。

#include <stdio.h>

int main(void) // 不推荐使用
{
    int i;
    int x[2][5] = { 1,2,3,4,5,6,7,8,9,10 };

    int(*p_x)[2];

    for (p_x = x; p_x < &x[1][5]; p_x++)
    {
        printf("%d  ", *p_x[0]);
    }
    return 0;
}

在Visual Studio 2019中运行的结果为

1  3  5  7  9

或者可以采用下列的方法来实现步长为2的访问。

以2为步长进行移动
#include <stdio.h>

int main(void)
{
    int i;
    int x[2][5] = { 1,2,3,4,5,6,7,8,9,10 };

    int* p_x = &x[0][0];

    for (; p_x < &x[1][5]; p_x += 2)
    {
        printf("%d   ", *p_x);
    }

    return 0;
}

在Visual Studio 2019中运行的结果为

1   3   5   7   9

上述几种方法实现了对一个最后一维为5的多维数组的步长为2的元素访问,但是实际项目中,我们并不会这么做,这种访问违背了我定义一个二维,每一维为5个元素的多维数组的元素访问初衷,我们并不希望通过一个指向的元素个数不等于多维数组一行数据个数的指针来访问数组元素。所以,这种方法要避免使用。

多维数组名代表指针

若不使用下标,则可以直接引用多维数组名代表指针变量,它时一个指针最后一维长度的数组的指针,例如顶一个维度为2x5的数组

int x[2][5];

则可以不使用任何下标引用x,此时,x代表指向一个包含5个整型元素的数组的指针。每次将x增加或者减少1,都将向前或者向后移动5个整型元素大小。


#include <stdio.h>

int main(void)
{
    int i, j;
    int x[2][5] = { 1,2,3,4,5,6,7,8,9,10 };

    for (i = 0; i < 2; i++)
    {
        for (j = 0; j < 5; j++)
        {
            printf("%d   ", *(*(x + i) + j)); // 其中x+i就表示移动i*5个元素
        }
    }

    return 0;
}

在Visual Studio 2019中运行的结果为

1   2   3   4   5   6   7   8   9   10

函数参数中使用指针

函数参数传址

C语言的函数参数可分为传值和传址,其中,对于非复合形式的非指针数据,在函数内部会生成参数的复制版,对这个复制版所作的所有修改,在函数退出后,都将失效,也就是说修改无法改变参数本书的值。原因在于,这个复制版本身存储在程序栈种,程序运行完毕后会释放栈空间。

传址是指参数是复合类型(数组,结构等)或者指针,传递给函数的是参数的内存地址,利用该地址,可以改变参数的值。

下面的程序通过参数传址来交换两个整型变量的值

#include <stdio.h>

int myswap(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(void)
{
    int result;
    int x = 50;
    int y = 30;
    myswap(&x, &y);
    printf("x:%d-y:%d\n", x, y);
    
    return 0;
}

程序在Visual Studio 2019下运行的结果为

x:30-y:50

说明x与y成功地通过传址实现了内部的值的交换

下面这个例子通过传址实现数组求和

int mysum(int length, int* data)
{
    int myresult = 0;
    int i;
    for (i = 0; i < length; i++)
    {
        myresult += *(data + i);
    }
    return myresult;
}

int main(void)
{
    int result;
    int x[5] = { 1,2,3,4,5 };
    result = mysum(5, x);
    printf("%d\n", result);

    return 0;
}

在Visual studio 2019中运行的结果为

15

程序中mysum函数接受2个参数,第一个参数是数组长度,第二个参数是指向数组的指针,目前没有较好的检查C语言的数组访问越界问题,所以最好的办法就是把数组的长度直接传给被调用的函数。

常量指针

字符串常量指针

字符串常量可以直接作为指针基址,加上偏移步长(向右跳过的字符数),可以得到余下的字符串的起始地址,

#include <stdio.h>

int main(int argc, char** argv)
{
    printf("%s\n", "abcdefgh" + 2);
    return 0;
}

在Visual Studio 2019中运行后得到的结果为

cdefgh
const指针

const类型定义的指针表示它指向的变量或对象的值是不能被修改的。const指针主要分为三类

指针指向的内容不可变,但指针本身可以改变

这类指针的特点就是指针指向的地址可以改变,但地址所在的内容不能变,声明的时候采用如下的方法

const int *a; // *号在const的右边
int const *a; // *号在const的右边

两种方法都可以。下面的程序声明两个指针,一个是非常量指针pr,一个是常量指针cpr。

#include <stdio.h>

int main(int argc, char** argv)
{
    int a[] = { 12,33,44,55,66,77,88,99 };
    int* pr;
    for (pr = a; pr <= &a[7]; pr++)
    {
        (*pr)++;
    }
    const int* cpr;
    for (cpr = a; cpr <= &a[7]; cpr++)
    {
        printf("%d\n", *cpr);
    }

    return 0;
}

程序运行的结果为

13
34
45
56
67
78
89
100

下面强行改变常量指针cpr指向的内容试试看。

#include <stdio.h>

int main(int argc, char** argv)
{
    int a[] = { 12,33,44,55,66,77,88,99 };
    const int* cpr;
    for (cpr = a; cpr <= &a[7]; cpr++)
    {
        //(*cpr)++;   // 强制修改const指针指向的内容,编译不通过
        printf("%d\n", *cpr);  
    }

    return 0;
}

结果在Visual Studio 2019 中程序编译报错,无法运行。

指针本身不能变,但指向的内容可以改变。

这类指针的特点是指针指向的地址不能改变,但是地址所在的内容可以改变。声明的时候采用如下的方法

int *const a  // *号在const的左边

程序如下,

#include <stdio.h>

// 这种指针声明:指针指向的地址不能改变,但地址所在的内容是可以改变的
void plusplus(int* const cpr)
{
    (*cpr)++;
}

int main(int argc, char** argv)
{
    int a[] = { 12,33,44,55,66,77,88,99 };
    int* pr;
    for (pr = a; pr <= &a[7]; pr++)
    {
        plusplus(pr);
        printf("%d\n", *pr);
    }
    return 0;
}

程序在Visual Studio 2019的输出如下

13
34
45
56
67
78
89
100

然后尝试着去改变指针指向的地址。

#include <stdio.h>

int main(int argc, char** argv)
{
    int a[] = { 12,33,44,55,66,77,88,99 };
    int* const cpr;
    //for (cpr = a; cpr <= &a[7]; cpr++)  // 不能修改const指针指向的地址,编译失败
    {
        //printf("%d\n", *cpr);
    }
    return 0;
}

Visual Studio 2019 编译报错,const指针指向的地址无法被修改。

指针本身不能改变,指向的内容也不能改变

这类指针的特点是指针指向的地址不能改变,且地址所在的内容同样不能被改变。可以用以下的方式声明:

const int *const a; // 既在const之后存在*号,也在const之前存在*号

下面的程序尝试去修改这个指针的地址和它保存的地址所指向的值

#include <stdio.h>

int main(int argc, char** argv)
{
    int a[] = { 12,33,44,55,66,77,88,99 };
    const int* const cpr;
    for (cpr = a; cpr <= &a[7]; cpr++)  // 指向指向的地址无法被修改,编译失败
    {
        (*cpr)++;  // 指针指向的内容无法被修改,编译失败
        printf("%d\n", *cpr);
    }
    return 0;
}

在Visual Studio 2019中,发现编译失败,无法运行,无论是修改指针地址还是修改指针指向的值,均编译时报错。

函数指针

函数指针

C语言中的数据变量无论是在程序栈还是堆中,都拥有自己的内存地址,函数也一样,函数的代码也需要调入内存才能够被执行,它们在内存中也拥有自己的起始地址,因此可以定义指针指向函数,存储函数的起始地址。可以用如下的方式声明。

int (*fun)(int a, float b)

用上述的方法定义函数指针,第一个int为函数的返回类型,fun为函数指针变量名,随后的括号里跟的是参数列表。下面的例子定义一个函数指针,函数指针指向add函数,然后使用函数指针而非函数名来调用函数。

#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

int main(void)
{
    int (*myfunc)(int a, int b);  // 定义函数指针
    myfunc = add;   // 函数指针指向add函数
    int x = myfunc(12, 36); // 使用函数指针调用函数
    printf("%d", x); 
    return 0;
}

在Visual Studio 2019中执行上述程序后得到的结果为

48

利用函数指针机制,能让C语言模仿C++类,实现某种程度上的面向对象编程,如下例,定义一个structure,里面包含几个结构体成员变量和一个函数指针

#include <stdio.h>

struct mynum
{
    int a;
    int b;
    int result;
    void (*mod_add)(int a, int b, int* result);
};

void madd(int a, int b, int* result) 
{
        (*result) = (a + b) % 13;    
}

int main(void)
{
    struct mynum mnum; // 定义结构体
    mnum.a = 12;  // 对结构体成员变量赋值
    mnum.b = 26;
    mnum.mod_add = madd;  // 指定结构体中包含的函数指针指向的函数
    mnum.mod_add(mnum.a, mnum.b, &mnum.result); 
    printf("%d\n", mnum.result);
    return 0;
}

在Visual Studio 2019中运行结果为

12

这样子,就在C语言中借助函数指针模仿了C++中类。

函数指针数组

函数指针数组是指以某数组的元素为指针,这些指针均指向函数的起始地址。这样做的好处是,可以先定义若干函数,然后将这些函数的起始地址放入函数指针中,这样就可以通过指针数组中的元素,方柏霓地调用线管地函数执行。函数指针数组定义的方法为

返回类型 (* 函数指针变量名[]) (参数列表)

下面的例子利用函数指针数组完成了四则运算。

#include <stdio.h>
#include <stdlib.h>

int add(int a, int b)
{
    return a + b;
}

int sub(int a, int b)
{
    return a - b;
}

int main(int argc, char* argv[])
{
    int (*operate_func[])(int, int) = { add, sub };

    int myresult = 0;
    int oper = atoi(*++argv);

    printf("%d\n", oper);
    int mynum;
    while (*++argv != NULL)
    {
        mynum = atoi(*argv);
        printf("%d ", mynum);
        myresult = operate_func[oper](myresult, mynum);
    }
    printf("\n%d\n", myresult);

    return 0;
}

在Visual Studio 2019中运行结果为

0

0

文件指针

文件指针及操作函数

C语言通常用一个指针变量指向一个文件,该指针称为文件指针,通过文件指针就可以对它指向的文件进行各种操作。文件指针定义的方法为

FILE* 指针变量标识符

其中FILE为大写。FILE是由系统定义的一个结构,该结构含有文件名,文件状态和文件当前位置等信息。

下面是一些文件操作的常见函数:

  • fopen函数打开文件
  • fclose函数关闭文件
  • fgets函数读取文件的一行
  • fgetc函数读取文件的一个字符
  • fputs向文件写入字符串
  • fputc向文件写入一个字符
文件指针实例

下面这个例子通过打开文件,读取文件内容来看文件是怎么操作的

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char** argv)
{
    int exit_status = EXIT_SUCCESS;
    while (*++argv != NULL)
    {
        FILE* input = fopen(*argv, "r");
        if (input == NULL)
        {
            perror(*argv);
            exit_status = EXIT_FAILURE;
            continue;
        }
        printf("\n%s 内容如下: \n", *argv);
        int ch;
        while ((ch = fgetc(input)) != EOF)  // 也可以使用 while (fgets(mytext, 500, input) != NULL)来每次读取一行
        {
            printf("%c", ch);
        }
        if (fclose(input) != 0)
        {
            perror(*argv);
            exit_status = EXIT_FAILURE;
        }
    }
    return exit_status;
}

下面这个例子,追加内容到文件。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char** argv)
{
    int exit_status = EXIT_SUCCESS;
    while (*++argv != NULL)
    {
        FILE* output = fopen(*argv, "a");  // 文件以"a"的模式打开,每次往文件中写入时,都是追加到后尾。
        if (output == NULL)
        {
            perror(*argv);
            exit_status = EXIT_FAILURE;
            continue;
        }

        char mytext[500];
        int ch = '\n';

        while (1)
        {
            printf("请输入文字:");
            scanf("%s", &mytext);
            if (strcmp(mytext, "%end%") != 0)
            {
                fputs(mytext, output);  // 读取的内容写入到文件
                fputc(ch, output); // scanf函数不会读取换行符,因此加上换行符
            }
            else
                break;
        }
        if (fclose(output) != 0)
        {
            perror(*argv);
            exit_status = EXIT_FAILURE;
        }
    }
    return exit_status;
}

这样所输入的内容就被追加到了文件末尾。

总结

指针是C语言的核心,使用得当会大大提高程序的编写与运行效率。在C语言中指针不仅仅可以表示变量的地址,而且还可以存储数组,数组元素,函数,文件设备的地址等。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

轻舞飞扬SR

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值