内存和地址

内存

在生活中,一栋楼里面有若干个房间,每个房间有其对应的门牌号。

类似的,在计算机中,会把内存分成若干个内存单元。每个内存单元的大小取一

个字节。每一个内存单元都会有一个唯一的编号。

C语言给内存单元的地址起了一个新名字叫指针。

指针变量和地址

& 取地址符

C语言中创建变量其实就是向内存申请空间。

整型变量是向内存申请了4个字节的。

要拿到这个整型变量的地址就需要用到 & 取地址操作符。

& 操作符取出的是所占字节中地址较小的字节的地址。

指针变量和取地址操作符

指针变量

用于存放指针的变量称为指针变量。

1
2
int a = 10;
int* p = &a;

存放在指针变量中的值都会被认为是地址。

如何拆解指针类型

1
2
   int a = 10;
int* p = &a;

这里 可以将 int * 分开来看, * 说明变量 p 是一个指针类型变量,int 说明 p 这个 指针变量指向的是 int 类型的变量。


解引用操作符

将地址保存于指针变量中,要使用的话就需要用到解引用操作符 *

1
2
3
4
int a = 10;
int* p = &a;
*p = 20;
printf("%d\n", a);

指针变量的大小

指针变量的大小与平台环境的位数有关。

32位平台下地址是32个bit位(4个字节)
64位平台下地址是64个bit位(8个字节)

1
2
3
4
printf("%zd\n", sizeof(char*));
printf("%zd\n", sizeof(short*));
printf("%zd\n", sizeof(int*));
printf("%zd\n", sizeof(double*));



指针变量的大小和类型是无关的,在相同平台下大小都是相同的。

指针变量类型的意义

指针的解引用

通过调试来看看下面的代码

1
2
3
int a = 0x11223344;
int* pa = &a;
*pa = 0;


1
2
3
int a = 0x11223344;
char* pa = &a;
*pa = 0;

可以看到前者会将4个字节全部改为0,但是后者只会将第一个字节改为0。

这说明指针变量类型决定了解应用的时候有多大权限(操作几个字节)

指针 + - 整数

1
2
3
4
5
6
7
8
9
10
11
12
int a = 10;
char b = 'A';
int* pa = &a;
char* pb = &b;

printf("%p\n", pa);
printf("%p\n", pa + 1);
printf("%p\n", pa - 1);
printf("\n");
printf("%p\n", pb);
printf("%p\n", pb + 1);
printf("%p\n", pb - 1);


指针类型决定了指针向前向后走一步的距离

void* 指针

可以接受任意类型的地址。但是不能直接进行指针的 + - 和解引用运算

一般 void* 类型指针多用于函数参数的部分

const 修饰指针

const 修饰变量

const 在修饰变量时,变量是无法被直接修改的

1
2
const int n = 10;
n = 20;

但是可以通过指针变量绕过进行修改

1
2
3
4
const int a = 10;
int* pa = &a;
*pa = 100;
printf("%d\n", a);

const 修饰指针变量

1
2
3
4
5
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;
p = &m;

1
2
3
4
5
int n = 10;
int m = 20;
int* const p = &n;
*p = 20;
p = &m;

1
2
3
4
5
int n = 10;
int m = 20;
int const* const p = &n;
*p = 20;
p = &m;

可以发现

1
const 放在 * 的左边时,修饰的是指针变量所指向的内容,指针变量本身可以改变,所指向内容不允许修改
1
const 放在 * 的右边时,修饰的是指针变量本身,指针变量本身不允许改变,所指向内容允许修改
1
当 * 两边都有 const 时,指针变量本身和所指向的内容都不允许修改。

指针运算

指针 +- 整数

数组在内存中时连续存放的,只要知道第一个元素的地址,就能获得这个数组的所有元素

1
2
3
4
5
6
int arr[5] = { 1,2,3,4,5 };
int* p = &arr[0];
for (int i = 0;i < 5;i++)
{
printf("%d ", *(p + i));
}

指针-指针

得到的是两个指针变量之间相隔的元素个数,不是内存字节差

1
2
3
4
5
6
7
8
9
10
int arr[5] = { 1,2,3,4,5 };
int* p1 = &arr[0];
int* p2 = &arr[1];
int* p3 = &arr[2];
int* p4 = &arr[3];
int* p5 = &arr[4];
printf("%d\n", p2 - p1);
printf("%d\n", p3 - p1);
printf("%d\n", p4 - p1);
printf("%d\n", p5 - p1);

指针的关系运算

><:比位置,仅限同数组

== !=:比地址,任意同类型都能用

1
2
3
4
5
6
7
8
9
10
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < arr + sz) //指针的大小比较

{
printf("%d ", *p);
p++;
}

野指针

野指针指向的位置是位置的。

成因

指针变量不进行初始化

1
int* p;

数组越界访问

1
2
3
4
5
6
7
8
int arr[5] = { 1,2,3,4,5 };
int* p = &arr[0];
for (int i = 0;i <= 5;i++)
{
// i = 5 时,会出现越界访问,此时 p 为野指针
printf("%d ", *(p + i));
}
return 0;

指针指向的空间释放

1
2
3
4
5
6
7
8
9
10
11
12
13
int* Exp()
{
int n = 1;
return &n;
}

int main()
{
int* p = Exp();
printf("%d\n", *p);
return 0;
}

这里的 &n 只是函数内部的一个局部变量,当这个函数执行完成,这个局部变量

的地址就被释放了。

assert 断言

当指针变量为 NULL 时,它并不指向任何空间。

assert.h这个头文件定义了宏 assert(),如果不符合,就报错并终止运行。

这个宏常常被称作断言。

1
assert(p != NULL);

程序运行到这一步时会判断 p 是否为 NULL, 如果等于 NULL,程序就会直接停止运行并报错。

使用 assert() 不仅能自动标识文件出问题的行号,还有一种无需更改代码就能开启或关闭 assert()的机制。

1
#define NDEBUG


编译器读到 NDEBUG 时就会禁用文件中所有的 assert()

传址调用

在前面的文章中提到过形参和实参的关系。

形参是实参的一份临时拷贝。

对形参的操作不影响实参(适用于传值调用)

而传址调用,对形参的操作会影响到实参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Swap1(int a, int b)
{
// 传值调用 交换失败
int c = a;
a = b;
b = c;
}
void Swap2(int* a, int* b)
{
// 传址调用 交换成功
int c = *a;
*a = *b;
*b = c;
}


调试看到进入 Swap1() 后 x ,y 和 a , b 的地址并不相同。


进入 Swap2() 后 x 和 y 中存放的地址与 a ,b 相同。

所以 Swap1() 交换失败, Swap2()交换成功。

如果想在函数中的操作影响到实参,就用传址调用。

反之就用传值调用。