栈:就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆:就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
自由存储区:就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
全局存储区(静态存储区):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
每个内存单元都有一个编号. 内存单元的编号称为地址
这里要区别一下我们看到的变量和计算机看到的变量的概念
比如说
int a = 10;
在我们可以这么认为
a
10 |
这种直接通过变量名来获取或者改变值的方式, 叫做直接访问.
但是我们直到计算机内存是一个只有数字0 和1的世界. 怎样0和1组成的二进制数来表示10这个数据呢
事实上在我们声明一个变量的时候, 系统会自动在内存里开辟一个和变量相对应大小空间
比如上面的代码 在声明一个int型变量的时候系统会开辟 4个字节大小的空间, 空间里的值是10 换算成二进制数就是 1010, 其余高位补零
a
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
这里我们要弄清楚变量名和物理地址的关系, 事实上, 变量名是高级语言中定义的一种东西, 它的作用就是为了方便的寻找地址.在编译了以后,它就变成了它对应的物理地址.所以事实上, 我们调用变量名, 就等于调用了这个变量的物理地址.
然而这个物理地址我们还是可以获取到的. 通过 取地址符 & 可以直接访问到变量a的物理地址. 这个地址符, 我们大家都不陌生, 在格式输入函数scanf("%d", &a); 中常用到, 至于为什么要用& 呢, 下文中将会提.
指针变量
什么是指针, 指针就是地址.
那么什么是指针变量, 显而易见, 就是保存值是地址的变量就叫做地址变量. 指针变量占8个字节, 它所保存的内容是一个物理地址的值, 也就是一串16进制的常数.
指针的声明方法如下
int* p = NULL; 此时的* 是无意义的, 它仅仅告诉系统变量p是一个指针变量.
为什么要有指针变量这种东西呢?
接下来谈谈我浅薄的理解.
在C语言中, 变量是常量, 是不可改变. 而指针变量的正是为了解决常量不可变的问题, 通过另外一种间接的方式, 将地址这个变成变量的值, 通过改变值 来回避常量不可变的问题.
听着有点绕, 看以下分析.
在C语言中, 变量之间的赋值过程 是拷贝值的过程
比如
int a = 5;
int b = a;
a = 10;
printf("a=%d, b=%d", a, b);
先将5赋值给变量a,
把变量a的值赋值给变量b
再将10赋值给变量a
打印结果发现a=10, b=5.
这里大家思考一下, 变量b的值, 是来源于变量a的, 为什么a的值变了, b的值没有改变呢.
这里其实就是一个拷贝的过程.
int a = 5;
开辟了一个int型所占大小的空间
a:
5 |
int b = a;
开辟了另一个int型所占大小的空间, 它的值和a的值一样. 所以把a的值再写到b的空间里
b:
5 |
同样的道理, 实参和形参之间也是拷贝的关系.
注意到函数的写法.
void exChange(int a, int b) {
int temp = a;
a = b;
b = temp;
}
大家注意到, 在形参列表里, 有一个变量声明的过程 int a, int b
这两句的作用是在内存中开辟了两个int型所占大小的空间, 这两个空间名分别是a, b.他们的值, 是通过函数调用时, 实参的值通过拷贝传过来的.
这意味着, 实参和形参是分开在两个地方的不同空间, 他们之间的关系只有值相同.注意这一句话.
这会导致什么问题呢?
大家都知道变量的作用域, 形参的作用域就是在函数里, 函数里的代码走完跳出去了以后, 形参的空间就被系统回收了, 也就是说形参里的值, 在函数里的计算结果就消亡不在了.
比如上面这个函数, 它的作用从代码上看, 是为了交换两个参数内的值.
但是当我们在main函数中调用这个函数的时候, 大家都知道结果是交换失败了.
为什么呢?
假设在主函数中的调用语句如下:
int x = 5, y = 6;
exChange(x, y);
printf("x=%d,y=%d",x,y);
为了避免混淆和理解困难,这里我们在主函数中声明了两个int型变量, x和y(和形参的名字不同)
于是系统给这两个变量开辟了两个空间. 分别存放了5 和 6
现在我们来理一下内存中空间开辟的顺序关系:
int x = 5, y = 6;这句语句运行结束后
系统出现了两个空间
x空间
5 |
6 |
程序向下运行调用函数exChange()
这时候又为形参a, b开辟了两个空间
a
5 |
6 |
a 和 b的值为实参x, y拷贝而来
函数执行完毕后
a
6 |
5 |
但是函数调用结束后, a, b的空间被回收了
这时候实参的空间x, y里的值依然没有变化.
很头疼啊. 怎么样才能让实参的元素完成交换呢?
我们要注意到一点, 实参的值未能完成变化的本质原因是, 形参完成了交换功能后, 它们的空间被回收了. 实参的空间除了在拷贝值的时候被访问了一次以外, 再也没有重新赋值.
这时候在主函数里访问实参x, y的空间, 发现里面的值当然也没有发生变化.
怎么样让函数运行完以后, 实参的值也跟着变化呢?
分析本质原因, 其实我们只要在函数中, 改变实参地址里的值, 就可以达到交换数据的目的.
而要改变实参地址里的值, 当然首要的解决目标就是把 实参的地址告诉形参, 让形参在实参的地址里放肆的大概特改.
而通过指针变量就可以达到这一点
将函数改写成
void exChange(int *p, int *q) {
int temp = *p;
*p = *q;
*q = temp;
}
而调用方法是将实参的地址当成变量告诉实参
int x = 5;
int y = 6;
exChange(&x, &y);
这样就可以让形参直接访问到实参的地址而不是再另外一个会被回收的地址里做无意义的改动.
回来谈scanf()函数的问题.
通过以上的分析, 我们可以得到一个结论
要想改变一个值, 并且让这个值再另一个地方尤其是函数外可以看到并且更改, 只能在这个变量所在的地址里更改. 这也是为什么scanf()函数需要访问变量的地址的原因. 因为只有访问这个变量的地址, 并赋值, 才能在scanf函数外得到这个函数的值.
像指针变量这样, 通过访问变量内保存的地址, 再访问该地址对应的值的方法, 叫做间接访问. 而真是由于变量内的地址可以改变(也叫做重指向), 使得指针具有极强的灵活性.
事实上, 在我认为, 指针的存在, 就是为了解决C语言中地址是常量, 且不可更改而采取的绕道解决的策略. 不得不承认它是一个很强大的概念. 但是真的很绕, 初上手常常让人摸不着头脑, 只能多写多练.
以上是比人对指针浅薄里理解.
指针变量的赋值
指针变量的赋值, 相当于指针的重指向
int num1 = 50;
int num2 = 40;
int *p1 = &num1;
int *p2 = &num2;
p2 = p1;
*p2 = 100;
printf("%d ", num1);
printf("%d ", num2);
printf("%d ", *p1);
printf("%d ", *p2);
结果为100 50 100 100
原因是, p1一开始是指向 num1的地址, p2指向num2的地址
p2 = p1语句将p2重指向p1所指向的地址, 也就是num1的地址
这是 p1, p2 均指向num1的地址
*p2 = 100; 将num1里的值改为100 此时 *p1 num1 均一起改变变成100
接下去 来讨论一下指针变量的类型
首先弄清楚一点 指针变量保存的是地址 无论它的类型是什么, 它的大小都是8个字节
它的类型, 事实上是告诉系统它所指向的地址, 是属于什么类型的区域
这和指针算术运算是息息相关的
int a = 3;
int *p = &a;
p++;
printf("%d\n", a);
printf("%p\n", &a);
printf("%p\n", p);
发现p的值 比a的地址加了4
char a = 'a';
char *p = &a;
p++;
printf("%c\n", a);
printf("%p\n", &a);
printf("%p\n", p);
发现p的值 比a的地址加了1
即, 地址加1, 相当于加上一个相应数据类型所占地址长度
注意:
指针与数组
int array[4] = {1, 3, 5, 7};
数组名array 代表了 &array[0]
由于数组各元素之间的地址是连续的
只要获得数组的首元素地址, 和数组长度(或者数组的数据类型) 就可以地址联系的特性, 用指针把数组所有的元素打印出来
int *p = array; // 指针变量p保存的是数组首元素的地址
printf("%d\n", *p);
printf("%p\n", p + 1);
printf(“%d\n", *(p + 1));
printf("%d\n", *(array + 2));
在取元素的时候得注意一下 * 的优先级 *(p + 1) 和 *p + 1 结果截然不同
前者是取p下个地址的值, 后者是p地址的值 加上11
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
指针与字符数组
char str1[] = "iphone";
char *p = str1;
printf("%s\n", p);
printf("%c\n", *(p + 2));
*(p + 2) = 'W';
printf("%s", p);
int num = 0;
while (*(p + num) != '\0') {
num++;
}
printf("长度为:%d", num);
int num = 0;
while (*p != '\0') {
num++;
p++;
}
printf("长度为:%d", num);
指针数组
char *string[3] = {"iOS", "Android", "WinPhone"};
char **p = string;
for (int i = 0; i < 3; i++) {
printf("%s\n", *(p + i));
}
char *strings[3] = {"iOS", "Android", "WinPhone"};
printf("%s\n", strings[0]);
printf("%s\n", *strings);
printf("%s\n", *(strings + 1));
指针数组的应用:
在主函数中输入6个字符串(二维数组),对他们按从小到大的顺序,然后输出这6个已经排好序的字符串。要求使用指针数组进行处理。
void sortStrings(char *a[], int count) { // 传入的数据是指针数组, count为指针数组的大小
for (int i = 0; i < count - 1; i++) {
for (int j = 0; j < count - 1 - i; j++) {
int num = strcmp(a[j], a[j + 1]);
if (num > 0) { // 若是后者比前者大, 两者的指针交换
char *temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
char string[6][50];
char *strings[6];
for (int i = 0; i < 6; i++) {
strings[i] = string[i];
}
for (int i = 0; i < 6; i++) {
printf("请输入第%d个字符串:\n", i + 1);
scanf("%s", string[i]);
}
printf("排序前:\n");
for (int i = 0; i < 6; i++) {
printf("%s\n",strings[i]);
}
sortStrings(strings, 6); // 将指针数组当成参数传给函数
printf("排序后:\n");
for (int i = 0; i < 6; i++) {
printf("%s\n",strings[i]);
}
因为时间的和篇幅的关系, 本篇博客对指针数据的介绍并不详尽, 过段时间若有时间将会再详细整理一份.
指针对于初入门的C语言的朋友而言, 会是一道难关, 但是确实一个很好理解计算机运作的一个关节. 虽然在C语言后来的许多优秀语言中, 指针的概念被弱化了非常多, 也有了更多很好处理内存地址的方法. 但是指针还是一个很好的衡量一个程序员的标准. 不得不承认, 指针是在地址常量不可改的前提下一个极其优秀的处理策略, 望大家能够从本博中获得到那么一点有帮助的信息, 祝大家学习愉快.