1、基本数据类型变量名和内存的关系:

  int i;
  scanf_s("%d", &i);
  • 1
  • 2

int i;,在这一句就为i分配了内存(但尚未对这块内存进行初始化),所以可以通过&i直接使用这块内存。赋值就更不用说啦,i = 3;。

变量名i,是为方便编程人员使用,是这块内存的别名,指代到块内存,对编程人员i代表这块内存中存储的值(实际上是i指到这个内存,然后取值)。通常我们都是通过变量名来使用已知的内存的。

i代表取(这块内存中存储的)值,而&i代表取(这块内存的)址。程序本身是不为i这个变量名分配空间的。在最终的机器代码中,是不会出现变量名的,这一点在分析反汇编语言时可以看出(命令:dumpbin /disasm xx.obj >xx_disasm.asm可以查看反汇编语言)。那么编译器是如何处理变量名的呢,变量名会存储在符号表中,并将符号表中的索引对应到实际物理空间(地址)上去,当调用这个变量时,查找符号表就可以找到对应的地址并取值了。

2、不同类型变量的变量名和内存间的关系:

上面分析的是基本数据类型(如int、char等)的变量名。C中除了变量名之外,还有函数名、常量名、指针名、数组名、结构名等。和变量名不同,这些标识符都是直接对应着地址的。基本数据类型的变量,和地址的对应关系需要取址符&才能得到地址,而其余的这些,名字本身就对应着地址。

例如char *pc = “se”;,就是将字符串常量”se”的首地址(位于常量存储区)赋值给了字符指针pc。这也就解释了为什么不需要为pc分配地址就可以为其赋值,而不会遇到类似下面代码所带来的野指针问题:

  int *pi;
  *pi = 1;
  • 1
  • 2

int *pi句,是为pi分配空间,而不是开辟pi所指向的空间。

2.1 C语言中的常量:

C对常量是怎么处理的呢?比如上面的i = 3;中的常量3,存储常量3的地址并不是随机分配的,是在程序中的数据段中(.data?这个我也还不是很确定,希望知道的前辈们给个指导),也就是程序本身并不为3分配内存,而是直接写入指令。3是数字常量,对于字符常量和字符串常量,又分别是怎么处理的呢?

字符常量和数字常量是一样的处理方式,都是类似汇编中的立即数,直接写入指令;

而字符串常量,则是存储在常量存储区,可以使用&(“string”)取得具体地址。也就是字符串常量名字本身指代着地址,只是不能直接操作(和int i中的i相同)。

2.2 C语言的变量名

c中的数据类型除常量之外大致有5种:

基本数据类型:short、int、float、double、char:

对各基本数据类型的变量名及其声明时的处理方式都是一样的,声明时即分配内存,并使用变量名直接操作这段内存;使用取地址符号&取得地址的数字表示,至于声明时要不要做初始化,要看是不是全局变量或者 static变量了。

这类变量名指向一个地址空间,但不能直接当做地址使用,而是通过取址符&操作其地址。

2.3 构造数据类型:数组、结构、联合:

1) 数组

数组在声明时,即分配了空间:
  int a[5];
一旦声明a[5],相当于有a、a[0]、a[1]、a[2]、a[3]、a[4]这6个变量名。a[i]的指代方式和普通的变量名int i相同,指到一个对应的内存空间;关键是变量名a,本身就可以做地址用。我们知道a是数组名,但a并不代表整个数组的地址,而是指向数组首元素的地址(虽然在数值上是相同的,下面会有详细解释),所以可以有 int *p = a;。那么&a又怎么解释呢?对于int i而言,i代表一个空间,&i表示i所代表的空间地址;那么&a应该也是表示a所代表的地址了,也就是整个数组的地址。

a、&a和&a[0]同代表地址,且由于数组是顺序存储,所以a、&a和&a[0]所表示的地址在数据上是相同的,但是实际的指代意义却是不同的:

  • a是个int*类型的数据,相当于&(*a),是指向数组首元素的地址;
  • &a指代整个数组,是个int(*)[]类型的数据,是指针数组的地址;
  • &a[0]则是仅指代一个存储int的空间,是int*类型的数据。

也就是数组名,本身可以作为地址使用,指代该结构的首元素的地址。

2) 结构

结构在声明的时候,就分配了空间。结构体和数组不同,结构体类型的变量名并不能直接当作地址使用,这一点和基本数据类型相同。需要对结构体名使用取址符&才能进行地址操作,并且取址所得到地址代表的是指向结构体的指针,只是在数据上和结构体中的首元素地址相同。

对于结构体中的各个元素,其名称的指代关系和其数据类型相同,并不因为是结构体的元素而受到影响。具体见下面代码:

struct stu{

         int age;
         char sex;
         char* name;
         int score[5];
};

int main()
{
         int i;
         struct stu st1;              //st1是 结构体stu类型
         printf("%d\n", &st1);        //&st1是 stu*类型
         printf("%d\n", &st1.age);    //&st1.age是 int*类型,st1.age就是个int型,名字指向地址,但不能直接作地址
         printf("%d\n", &st1.sex);    //&st1.sex是 char*类型,名字解析同上
         printf("%d\n", &st1.name);   //&st1.name是 char**类型,st1.name是char*类型
         printf("%d\n", st1.score);   // st1.score是个数组类型,名字代表数组中首元素的地址

         return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

3) 联合

联合是特殊的结构体,为节省空间,在其各元素依次存储,各元素的首地址均相对变量的基地址偏移为0,具体各变量名的分析和结构体同。

2.4 指针类型

声明一个指针类型 int *p;,则是为存储指针p分配空间,而并不会为p所指向的内存做任何动作,这就是野指针的原因。如下代码,p就是一个未指向任何已知内存的指针,为*p赋值,自然会出现错误:

  int *p;
  *p = 1;
  • 1
  • 2

指针中,char 是个很特殊的指针。一般的指针,仅指向一个所定义类型的内存,而char 则可以指向一个字符串,之所以可以实现这个功能是字符串结尾符’\0’的存在标识了字符串的结束。如下的代码,就是将pc指向了“string”所指代的常量存储区地址。
  char *pc = “string”;
这也是char *pc = “string”合法,而int *p =1不合法的原因:”string”本身即代表了它的存储地址,而整型常量1仅仅是个操作数,并不是地址,如果希望使用数据为指针(指向的地址)赋值,可以使用一个强制转换 int*p = (int*)1,只是这样如果不加以检查的话,写出来的代码会存在安全隐患。因此,不管指针变量是全局的还是局部的、静态的还是非静态的,都应该在声明它的同时进行初始化,要么赋予一个有效的地址,要么赋予NULL。

另外,声明一个指针,只是在栈区为指针本身的存储分配了地址,而不限制指针所指向的内存到底是在栈区、还是在堆区、还是在常量存储区。这也就造成了 函数调用返回值 会因实现不同而有不同意义,是函数调用结束后返回值有效性不同的原因。

2.5 空类型

C中有void关键字,但其实C中是并没有空类型的。比如我们不能做如下定义:
void a;
因为C、C++是静态类型的语言,定义变量就会分配内存。然而,不同类型的变量所占内存不同,如果定义一个任意类型的变量,就无法为其分配内存。所以,C、C++中没有任意类型的变量。

但是定义void p;是合法的,void 所定义的p表示以指针,所指向的类型未定。因为void *p;声明是为指针p分配了空间,无论指针指向什么类型,存储指针所需的空间的固定的,所以不存在因为所需空间大小而无法为p分配空间的问题。

但void p的使用也是很受限制的,由于不知道其指向的数据类型,所以是不能对p进行自增操作的;void的主要作用有两点,一个是限制函数的返回值,一个是限制函数的参数类型;void 则常用于指针的类型转换。如下代码:

  int *pi;
  float *pf;
  • 1
  • 2

如果想将pi指向pf所指向的内存空间,就必须进行类型转换:pi = (int *)pf;。

而如果是将pi换成void *p,就不需要转换,可以直接为指针赋值。这样的直接赋值,只能是将一个已知类型的指针赋值给void *p,而不能是将void *p未加强制转换地赋值给一个已知类型的指针,如:

    void *p;
    int *pi;
    float *pf;
    p = pf;   // pf = p;就是非法的,不能将 "void *" 类型的值分配到 "float *" 类型的实体
    p = pi;
  • 1
  • 2
  • 3
  • 4
  • 5

但需要注意的是,即使进行了转换,p仍然是个void*类型的指针,不能对其进行sizeof(p)等涉及所指类型的操作,同样地p也不能直接用于具体数据类型的操作。如下面的代码中*p = 1.73; 和printf(“%f”, *p)都是非法的:

    void *p;
    float *pf;
    p = pf;

    *p = 1.73;           //*pf  = 1.73;合法
    printf("%f", *p);    //printf("%f", *pf); 合法
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这样说来,void *的意义何在呢?可以使用强制类型转换使用void *p作为中介,见下面的代码:

  float *pf;
  void *p;
  float f=1.6;
  p = (void*)&f;
  pf = (float*)p;
  • 1
  • 2
  • 3
  • 4
  • 5

这样,float pf就指向了float f所在的地址,但注意p依然不能直接使用。这个例子,只是为我们展示了void 有这样的功能,但平常代码中很少这样无意义地转换,更多地是将void *作为函数参数,这样就可以接受任意类型的指针了,典型的如内存操作函数memcpy和memset的函数,其原型分别为:

  void * memcpy(void *dest, const void *src, size_t len);
  void * memset ( void * buffer, int c, size_t num );
  • 1
  • 2

也可以编写自己的将void 作为函数参数的函数,由于char是C中最小长度的变量,其它任何变量的长度都是它的整数倍。可以使用char作为中转,详见下面的函数实现:

void swap(void *pvData1, void *pvData2, int iDataSize)
{
    unsigned char  *pcData1 = NULL;
    unsigned char  *pcData2 = NULL;
    unsigned char  ucTmp1;
    pcData1 = (unsigned char *)pvData1;
    pcData2 = (unsigned char *)pvData2;

   do{
        ucTmp1       = *pcData1;
        *pcData1     = *pcData2;
        *pcData2     = ucTmp1;
        pcData1++;
        pcData2++
        ;} while (--iDataSize > 0);
}

int main()
{
         float fa = 1.23, fb = 2.32;
         float *f1=&fa, *f2=&fb;
         int iDataSize = sizeof(float)/sizeof(char);
         swap(f1, f2, iDataSize);
         return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

NULL

C中对NULL的预定义有两个:

  #define NULL 0
  #define NULL ((void *)0)
并且标准C规定,在初始化、赋值或比较时,如果一边是变量或指针类型的表达式,则编译器可以确定另一边的常数0为空指针,并生成正确的空指针值。即在指针上下文中“值为0的整型常量表达式”在编译时转换为空指针。那么也就是上面的两个的定义在指针上下文中是一致的了。

我们经常在声明一个指针时,为避免野指针的情况常用的int pi = NULL;中的NULL,是会被自动转换为(void )0的。所以下面的代码也是合法的:

int *pi = 0;
if(pi == 0){
 ……
}
  • 1
  • 2
  • 3
  • 4

2.6 函数类型 和 函数指针

尽管函数并不是变量,但它在内存中仍有其物理地址。每个函数都有一个入口地址,由函数名指向这个入口地址,函数名相当于一个指向其函数入口的指针常量。

可以将函数名赋值给一个指针,使该指针指向这个函数的入口,即是函数指针。

这里注意和指针函数区分开来:

指针函数是一个返回指针的函数,指针函数具体定义方式:

char *Convert(char *pName , int length);

函数指针的定义要和具体所指向的函数的形式一致,如对函数int Max(int a, int b)定义一个函数指针:

  int (*pMax)(int a, int b);
  pMax = Max;
  • 1
  • 2

int (pMax)(int a, int b)句中,函数指针pMax外的括号一定要带上,因为“()”的优先级高于“”,如果无括号,就变成了int *pMax(int a, int b)的形式,变成了一个函数(指针函数)的声明了。pMax=Max句将代表函数int Max(int a, int b)入口地址的其函数名Max,赋值给了指向同类型函数的指针pMax。这样pMax就和Max有相同的指代作用,并且pMax还可以指向与int Max(int a, int b)同参同返回值的函数。

通过括号强行将pMax首先与“*”结合,也就意味着,pMax是一个指针;接着与后面的“()”结合,说明该指针指向的是一个函数,然后再与前面的int结合,也就是说,该函数的返回值是int。由此可见,pfun是一个指向返回值为int的函数的指针。

  int Max(int a, int b);
  int Min(int a, int b);

  int (*p)(int a, int b);
   int max, min;
   p = Max;
   max = (*p)(3, 5);   //进行调用时,也要记得带括号(*p)
   p = Min;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

最后需要注意的是,由于函数在内存中的分布方式并不是齐整的,所以函数指针并没有++自增运算和—自减运算。

--------------------- 本文来自 wangjun_huster 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/wangjun_huster/article/details/69486956?utm_source=copy

10-07 14:19