C——指针与内存四区提高(1)

一、数组做函数参数退化问题

#include <stdio.h>

//void printfArr(int a[6], int num){
//void printfArr(int a[], int num){
void printfArr(int *a, int num){// 三种写法结果是一样的
    int i;
    int num2 = 0;

    num2 = sizeof(a) / sizeof(a[0]);
    printf("num2 = %d\n", num2);

    for (i = 0; i < num; i++){
        printf("%d ", a[i]);
    }
}

int main() {
    int num1 = 0;
    int arr[] = {12,34,1,3,45,23};
    num1 = sizeof(arr) / sizeof(arr[0]);
    printf("num1 = %d\n", num1);
    printfArr(arr, num1);
    system("pause");
}

这里写图片描述

结论:
多数情况下,C将数组名视为数组第一个元素的地址,数组名做函数参数会退化为一个指针。也就是说形参中的数组编译器会把它当成指针处理。

一种例外情况是,将sizeof运算符用于数组名时,将返回整个数组的字节长度。而sizeof一个指针,只会返回一个固定的值,一般为4个字节。

实参arr和形参a的数据类型本质不一样,所以sizeof的结果不一样。

遍历数组需要将数组地址连同数组长度一起作为入参传进去。
形参写在函数上和写在函数内作用是一样的,只不过写在函数上具有对外的属性而已(也就是说可以通过实参传给形参的方式来初始化它)。

二、数组名与指针探讨数据类型本质

int main(){

    int arr[10] = {12,34,1,3,5};
    int *p = &arr;

    printf("arr = %d\n", arr);
    printf("arr+1 = %d\n", arr + 1);
    printf("&arr = %d, p=%d\n", &arr, p);
    printf("&arr+1 = %d, p+1=%d\n", &arr + 1, p+1);

    system("pause");
    return 0;
}

这里写图片描述
结论:
arr代表数组首元素的地址。
&arr代表整个数组的地址。

arr+1 与 &arr+1 的结果不一样,是因为arr与&arr所代表的数据类型不一样,也可以说是指针的步长不一样。arr是一个int类型指针(* int)(是一个4个字节的内存地址),&arr是int数组类型指针(int (*)[10])(是一个40个字节的内存地址)。

c/c++编译器把数组名(arr)处理为一个常量指针,所以不能修改它,arr = arr+1会报错,p = p+1则不会报错。

经典总结:数据类型的本质就是固定大小内存块的别名。
数据类型是C/C++编译器为了方便的表达现实生活中的人事物而引入的一个概念。

三、变量的本质

经典总结:变量的本质就是(一段连续)内存空间的别名(是一段内存空间的编号,类似于一个门牌号)。

既然数据类型和变量都是内存块的别名,这段内存块就可以有多个别名,那么如何增加新的别名呢?如下:
(1)、对数据类型取别名:

typedef int INTEGER

INTEGER a = 10;

// int、INTEGER都是int所代表的的数据类型的别名
typedef struct Teacher
{
    char name[64];
    int age;
} Teacher;

Teacher t1;

(2)、对内存空间取别名:

引用就是某一变量(目标)的内存空间的一个别名,对引用的操作与对变量直接操作完全一样。

  引用的声明方法:类型标识符 &引用名=目标变量名;
 

 int a; // 假如变量a指向的int型内存空间首地址为0x36345
 int &ra=a; //定义引用ra,它是变量a的引用

以0x36345开始的4个字节的这个内存地址,刚才他有个别名a,现在又有个别名ra了。

说明:

  (1)&在此不是求地址运算,而是起标识作用。
  (2)类型标识符是指目标变量的类型。
  (3)声明引用时,必须同时对其进行初始化。
  (4)引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。
   ra=1; 等价于 a=1;
  (5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra与&a相等。
  (6)不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。

四、内存四区

这里写图片描述

这里写图片描述

栈的属性和buf地址增长方向是两个不同的概念

一般情况下:栈的生长方向开口向下,堆的生长方向开口向上

void main() {
    char buf[128]; // 静态绑定的时候,buf代表的内存空间的标号就已经定义下来了
}

不管栈开口向上还是向下,buf内存地址永远向上的。

五、函数调用模型

这里写图片描述

六、指针专题大纲

铁律1: 指针也是一种数据类型

(1)、指针变量和它指向的内存块是两个不同的概念

int *p = &a;

a、给p赋值p=0x12345; p = p+1; 只会改变指针变量的值,不会改变所指向的内存块的值。
b、给*p赋值*p=1111; 不会改变指针变量的值,只会改变所指向的内存块的值。
c、*p放在等号左边——>给内存赋值 ——> 保证所指向的内存块能修改
d、*p放在等号右边——>从内存获取值

(2)、指针也是一种数据类型,是指它指向的内存空间的数据类型
指针可以看做是一种复合数据类型,依附于它所指向的内存空间的数据类型的一种数据类型。

指针步长(p++),根据所指内存空间的数据类型来确定。
如:

intarr[4] = {2,3,4,5};
arr++; // 指向数组首元素的地址,该地址段代表的数据类型是int类型,所以步长是4
&arr++;// 指向整个数组的地址,该地址段代表的数据类型是数组类型,所以步长是4*4=16

经典总结:
指针指向谁,就把谁的地址赋值给指针。
指针的数据类型决定了指针的步长。

(3)野指针产生的原因
指针变量和它所指向的内存空间变量是两个不同的概念。
释放了指针所指的内存空间, 但是指针变量本身没有重置为null,造成释放的时候出现野指针。

char * p = NULL;
p = (char *)malloc(100);
if (p ==NULL){
    return;
}


if (p !=NULL){
    free(p);// 第一次free没有问题,释放了指针p所指向的内存空间
    // p = NULL; 为避免野指针,此处应置为NULL
}

if (p !=NULL){// 指针变量p还是指向之前那块内存空间,此时p变成了野指针
    free(p);// 释放未知的内存空间,出错
}

避免方法:定义指针的时候,初始化为null,释放指针所指的内存空间后,重置指针变量为null。

铁律2: 间接赋值(*p)是指针存在的最大意义

函数调用时,用实参取地址传递给形参,在被调用函数里面用*p来改变实参,这样就相当于把运行结果传递出来了。

通过把内存的首地址以形参的方式传递过去,实现不同的函数可以同时操作一块内存空间。

间接赋值是指针做函数参数的精华,主函数和被调函数直接通过内存交换运算结果。

参考示例:

#define _CRT_SECURE_NO_WARNINGS 
#include "stdio.h"
#include "stdlib.h"
#include "string.h"

// 指针做函数参数,使用输入输出特性的
int getMem(char **param1/*out*/, int *length1/*out*/, char **param2/*out*/, int *length2/*out*/) {
    int result = 0;

    int mLen1, mLen2;
    char *mP1, *mP2;

    mP1 = (char *)malloc(100);
    strcpy(mP1, "123456");

    // 通过形参改变实参的值
    *length1 = strlen(mP1);// 一级指针 间接赋值
    *param1 = mP1;// 二级指针 间接赋值,

    mP2 = (char *)malloc(100);
    strcpy(mP2, "abcde");

    *length2 = strlen(mP2);
    *param2 = mP2;

    return result;
}

int main() {

    char *p1 = NULL;
    int len1 = 0;
    char *p2 = NULL;
    int len2 = 0;

    int result = getMem(&p1, &len1,&p2, &len2);// 通过函数调用,求得四个实参的值

    if (result != 0) {
        printf("fun getMem() error:%d", result);
    }

    printf("p1=%s \n", p1);
    printf("len1=%d \n", len1);
    printf("p2=%s \n", p2);
    printf("len2=%d \n", len2);

    // 释放堆内存
    if (p1 != NULL) {
        free(p1);
        p1 = NULL;
    }
    if (p2 != NULL) {
        free(p2);
        p2 = NULL;
    }

    system("pause");
    return 0;
}

运行结果:
这里写图片描述

间接赋值推论
在函数调用时:
用1级指针形参,去间接修改了0级指针(实参)的值。
用2级指针形参,去间接修改了1级指针(实参)的值。
用n级指针形参,去间接修改了n-1级指针(实参)的值。

铁律3: 理解指针必须和内存四区概念相结合

(1) 主调函数可以把堆区、栈区、全局数据区内存地址传递给被调函数。
(2)被调函数只能返回堆区、全局区数据。
(3)指针做函数参数,是有输入输出特性的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值