序言
C语言的第二个难点就是指针,是C语言的灵魂
为什么学习指针?
通过指针可以表示一些复杂的数据结构---链表,树,图
可以快速、高效的传递数据
在调用程序的时候能使函数返回一个以上的结果
可以直接访问硬件
可以非常方便的用来指向字符串,使字符串的处理更加灵活方便
指针是理解面向对象语言中‘引用’的基础
很多知识实际上内部都需要指针的知识。
要想确定一个数组需要两个参数:数组的首地址和数组的长度,但是要唯一地确定一个字符串只需要一个参数:字符串第一个字符的地址。因为系统会自动在字符串的末尾加上一个结束标识符‘\0’,字符串和指针的用法后续讲字符串的时候会细讲。
第一节、地址和指针的概念
要明白什么是指针,必须先要弄清楚数据在内存中是如何存储的,又是如何读取的。
在程序中一般通过变量名来对内存单元进行存取操作。
其实程序经过编译以后已经将变量名转换成变量的地址,对变量值的存取都是通过地址进行的。这种按变量地址存取变量的方式称为直接访问方式
还有一种简介访问的方式:变量中存放的是另一个变量的地址。
所以一个变量的地址就称为该变量的指针,指针就是地址,地址就是单元内存的编号,它是一个从0开始的、操作受限的非负整数。
操作受限:只有同一块空间中的指针和指针之间能进行相减运算,结果是一个常量(也就是这两个指针之间有几个元素)
内存中一个单元指的是一字节,占8位。每根地址有两种状态:0和1.两根地址总线就有四种组合(0 0,0 1,1 0,1 1),能控制四个内存单元;n根地址总线就有
种组合,能控制
个内存单元,即
字节。
如果有一个变量专门存放另一个变量的地址,那它就是指针变量
指针是一个地址,指针变量是存放地址的变量,习惯上我们把指针变量简称为指针。
第二节、指针和指针变量
为了表示指针变量和它所指向的变量之间的联系,在程序中用“*”表示“指向”。如果定义变量为指针变量,那么*i就表示指针变量i里面所存放的地址所指向的存储单元里面的数据。
2.1、指针变量的定义
C语言规定所有变量在使用前必须先定义,指定其类型,并按此分配内存单元。
指针变量定义的一般形式为:
基类型 *指针变量名;
例:int *i;
float *j;
1.*:表示该变量的类型为指针类型,指针变量名是i和j,不是*i和*j。
2.定义指针变量的时候,必须指定基类型,用来指定该指针变量可以指向的变量类型。比如int *i,表示i只可以指向int型变量。
3.基类型:"int *i;"其中i是变量名,i变量的数据类型是"int *"型,即存放int变量地址的类型。"int"+"*"才是变量i的类型,所以int称为基类型
4."int *i;"表示定义了一个指针变量i,它可以指向int型变量的地址。但此时并没偶给它初始化(该指针变量并未指向任何一个变量),就像"int j;"一样只是定义了,并没有给它赋初始值一样。
5.不同数据类型在内存中所占的字节数是不同的(1byte = 8bit),占几个字节就有几个地址,指针变量指向的是第一个地址,即首地址。因为通过所指变量的首地址和该变量的类型就知道该变量的所有信息。
byte 1byte
char 2byte
short 2byte
int 4byte
long 8byte
float 4byte
double 8byte
boolean 1byte
6.指针变量也是变量,是变量就有地址,所以指针变量本身也是有地址的,但是它的地址是系统为分配的,是指向指针变量的,而指针变量中的地址是指向地址中的内容。
地址中的值就像酒店门牌号里住的人一样,具体到了人,是内容
7.地址是可以运算的,比如后移1个位置或使指针+1.这里的1表示的是指针变量占几个字节,如果是int,那就是4个字节。那后移1个位置就是后移4个字节。+1就是地址加四个字节。
8.两个指针相减得出的结果是int类型的不是int *。
#include <stdio.h>
int main()
{
int *p,*q;
int k;
int i = 3,j = 4;
p = &i;
q = &j;
k = p-q;
printf("p = %d\nq = %d\nk = %d\n",p,q,k);
return 0;
}
2.2、指针变量的初始化
上面的代码其实已经用到了:int i = 3,*j; j = &i;,初始化就是用赋值语句使一个指针变量得到另一个变量的地址。
&:取地址运算符
*:指针运算符,功能就是取变量地址所指向变量中的内容。与&互为逆运算
注意:i!=j,j!=i。修改j不影响i,修改i也不会影响j。j是地址,i是内容。
定义指针变量时的*j和程序中用到的*j是不一样的,前者是一个声明,此时的*仅仅表示该变量是一个指针变量,并没有其它含义。后者将j指向变量i后,*j就i完全等同于i,可以互相替换。
由此可见*j==i,j里存放的是地址。
👆指针的第二种初始化方式。
指针变量和指针变量之间可不可以相互赋值呢?
#include <stdio.h>
int main()
{
int i = 3;
int *j,*k;
j = &i;
k = j;
printf("*j = %d\nj = %d\n*k = %d\nk = %d\n",*j,j,*k,k);
return 0;
}
👆明显是可以的,但是注意指针变量的类型一定是一样的,赋值的时候右边的必须初始化过了
2.3、指针常见错误
1.引用未初始化的指针变量,常见于用scanf给指针变量赋值。
#include <stdio.h>
int main()
{
//错误写法
/*
int *i;
scanf("%d",i);
*/
//正确写法
int *i,j;
i = &j;
scanf("%d",i);//因为i本身就是地址,所以不需要用&再取地址了
printf("*i = %d\n",*i);
return 0;
}
2.在空指针里写数据。
第三节、指针作为函数参数
3.1、互换两个数
“指针能使被调函数返回一个以上的结果”
#include <stdio.h>
void Swap(int a,int b);
int main()
{
int i = 3,j = 5;
Swap(i,j);
printf("i = %d, j = %d\n",i,j);
return 0;
}
void Swap(int a,int b){
int buf;
buf = a;
a = b;
b = buf;
return;
}
大家觉得i和j的值会互换吗?结果是不会,i还是3,j还是5.
因为实参和形参之间的传递是单向的,只能由实参向形参传递,函数调用完之后系统为其分配的内存单元都会释放。所以虽然将i和j的值传给了a和b,但是交换的仅仅是内存单元a和b的数据,对i和j没有影响。
以上传递方式叫作拷贝传递,即将内存1中的值拷贝到内存2中,不管内存2中的值发生怎样的改变都跟1没关系。
就跟别人抄你作业一样,他在他的作业本上怎么修改从你那抄过去的答案都不会改变你的作业本上写的答案
要想直接对内存单元进行操控,用指针最直接。
此时实参向形参传递的不是变量i'和j的数据,而是变量i和j的地址。这个也是拷贝传递,但是拷贝的是地址。
就像小组作业,几个人共同创造出来的作业,无论谁做了修改,影响的都是整个小组。
3.2、函数参数传指针和传数据的区别
如果希望在另外一个函数中修改本函数中变量的值,那么在调用函数时只能传递该变量的地址。
如果这个变量是普通变量,那么传递它的地址就可以直接操作该变量的内存空间。int i;i的地址就是&i。
如果是一个指针变量,那就直接传入,不需要再进行取址操作(就是不需要在变量名前面加&)
传指针比传数据节约内存、高效
在”1.数据很小,比如就一个int型变量。2.不需要嘎斯便它的值,只是使用它的值”时我们建议直接传递数据,其余建议传递指针。
3.3、定义只读变量:const
const在实际编程中用的不多,面试常考。
const是constant的缩写,意思是“恒定不变的”,定义常变量的关键字(拥有变量属性的常量)
定义方式:const int i = 10;<===>int const i = 10;
必须在定义的时候就赋值,不可以改变它的值(因为它是只读的)。被修饰的变量的生命周期是程序运行的整个过程。如果修饰的是局部变量,那么局部变量就具有了静态特性(并不是静态变量)。const修饰的变量存放在内存中的“只读数据段中”。
在c中,const修饰的变量不能做数组的长度,但是c++可以。
与define的区别:
1.define是预编译指令,const是普通变量的定义,const定义的变量有数据类型,define定义的对象没有。define定义的宏在预处理阶段展开,而const定义的只读变量是在编译运行阶段使用的。
2.define定义的是常量,const定义的是变量。define定义的宏在编译后就不存在了,它不占内存。
修饰指针变量的三种效果:(int a;int *p = &a;)
1.const int *p = &a;--->*p不可变,*p是内容,所以地址可变
2.int * const p = &a;--->p不可变,p是地址,所以内容可变
3.const int * const p = &a;--->内容和地址都不可变
注意:int *q; q = p;就算1、2、3定义了p/*p不可变,但是q没有被定义,那么修改q,p还是会变。const的约束只单单对它后边的一个变量起作用。就像小组作业你只能管的住自己手不去修改作业,管不住小组其他人的手一样。
第四节、指针和一维数组的关系
4.1、用指针引用数组元素
p = &a[0]表示将a[0]的地址放在指针变量p中,即指针变量p指向数组a的第一个元素a[0]。
C语言中规定数组名是一个指针常量,表示数组第一个元素的起始地址。所以p = &a[0]等价于p = a,它们存放的都是a[0]的地址。
起始地址:int类型占四个字节,就有四个位置,起始位置就是这四个位置的第一个位置,称为数组的首地址或数组第一个元素的起始位置。
注意:数组名不代表整个数组。q = a表示把数组a的第一个元素的起始地址赋给指针变量q。
4.2、指针的移动
C语言规定:如果指针变量p已经指向该数组的第一个元素,那么p+1就指向第二个元素。假如真int类型,那*(p+1)的地址是*p的地址+4(括号不能省)
👆得出:p[1] = a[1],p[2] = a[2].......
p[1] = *(p+1),p[2]=*(p+2)........
所以a[i] <==> *(a+i)
*(p+1)<==>* p++<==>* ++p,*(p-1)<==>* p--<==>* --p
a是常量,没有自加自减的功能
下面用指针循环输出数组数据:
#include <stdio.h>
int main()
{
int a[] = {1,2,3,4,5};
int *p = NULL;//指针初始化
for(p=a;p<(a+5);p++){
printf("%d\t",* p);
}
return 0;
}
小练习:输入10个整数,找出最大偶数,要求使用指针访问数组,自己尝试理解一下吧
#include <stdio.h>
int main()
{
int a[10];
int *p = a;
int max;
int i;
int flag = 1;//标志位
printf("请输入是个整数:");
for(i = 0;i<10;i++){
scanf("%d",p+i);//p已经是地址了,不需要再用&再取地址
}
for(;p<a+10;p++){
if(0 == *p%2){//能被2整除肯定是偶数
if(flag){//判断max有没有值,1没有0有
max = *p;
flag = 0;
}
else if(*p > max){
max = *p;
}
}
}
if(!flag){//如果flag一直是1那说明一个偶数都没有
printf("最大的偶数是:%d\n",max);
}else{
printf("没有偶数\n");
}
return 0;
}
4.3、两个参数确定一个数组
前面说数组的时候说过这个问题。下面和指针结合一些
#include <stdio.h>
void Output(int *,int);
int main()
{
int a[] = {1,2,3,4,5};
int b[] = {-1,-2,-3,-4,-5};
Output(a,5);
printf("\n");
Output(b,5);
return 0;
}
void Output(int *p,int len){
int *a = p;
for(;p<(a+len);++p){
printf("%d\t",*p);
}
}
在C语言中,所有指针变量,无论是什么基类型,在内存中都占4字节。
第五节、动态内存分配★
动态分配是和静态内存相对的。动态在堆上分配内存,静态在栈上分配。栈上分配的内存是由系统分配和释放的,空间有限,在符合语句或函数运行结束后就会被系统自动释放。堆上是由程序员自己手动分配和释放的,空间大,存储自由。
5.1、传统数组的缺点
1.数组长度必须事先指定,而且只能是常量
2.数组长度不能在函数运行的过程中动态扩充和缩小
3.数组所占空间不能由手动释放
5.2、malloc函数的使用
使用malloc函数需要引入头文件:stdlib.h
malloc和calloc函数是动态分配的标志
int *p = (int *)malloc(4):请求系统分配4字节的内存空间,并返回第一个字节的地址,然后赋给指针变量p。(int *)表示强制转换数据类型,没必要写,c中void *型可以不经转换直接赋给指针变量(函数指针除外),所以也可以写成:int *p = malloc(4);
下面的程序实现的功能是:调用被调函数,将主调函数中动态分配的内存中的数据放大10倍。
并不一定每一个计算机给int型变量分配的是4字节,也可能是8字节。如果是8字节,那我只分配4字节,剩下的4字节就会因为无家可归而占用邻居家。造成的后果是后面内存中原有数据被覆盖。最好的办法就是不把长度写死,而用sizeof(int),即:int *p = malloc(sizeof(int));或者:int *p = malloc(sizeof *p);
sizeof后面可以紧跟类型,也可以直接跟变量名。如果是变量名,那么就表示该变量在内存中所占的字节数。写类型的时候括号不能省略。
5.3、free函数的使用
动态分配的内存空间是由程序员手动编程释放的。
free函数无返回值。功能是释放指针变量p所指向的内存单元,此时p所指向的那块内存单元将会被释放并还给操作系统。p释放后仍然指向那块内存地址,只是那块内存空间已经不属于它了。所以当指针变量被释放后,要立刻把它的指向改为NULL。
释放:将该内存空间标记为可用状态。
#include <stdio.h>
#include <stdlib.h>
int main() {
int *p = (int *) malloc(sizeof*p);
*p = 10;
printf("p = %d\n", p);
free(p);
printf("p = %d\n",p);
return 0;
}
如果不释放内存会怎样呢?
如果是小程序那无所谓,程序运行结束后自动释放内存。但是像银行、中国电信这样的大程序,可能一个月维护一次,如果不及时释放内存,那么内存只会越用越少,最终造成内存泄漏。
注意:每一个动态内存只能释放一次,如果释放多次,程序会崩。
综上所述:malloc喝free一定是成对出现且一一对应的。free后一定要让指针变量指向NULL。
只有动态内存需要释放。静态内存只能由内存释放。
5.4、练习----动态数组的构建
1.指针的移动
#include "stdio.h"
#include "stdlib.h"
int main()
{
int i;
int *p = malloc(sizeof(*p)*5);
*p = 5;
*(p+1) = 10;
*(p+2) = 20;
*(p+3) = 30;
*(p+4) = 50;
for(i = 0; i < sizeof(*p)*5/sizeof(*p); i++){
printf("%d\t", *(p+i));
}
return 0;
}
输出结果:5 10 20 30 50
抛开静态内存和动态内存的不同,int *p = malloc(sizeof(*p)*5);等价于int p[5];
使用动态数组的优点就是长度可以动态指定,传统数组的长度只能在定义时指定,只能是常量。
#include "stdio.h"
#include "stdlib.h"
int main()
{
int cnt;
int *p;
int i;
printf("请输入你要存放的数组个数:\n");
scanf("%d", &cnt);
p = malloc(sizeof(int)*cnt);
printf("请输入数组元素的内容:\n" );
for(i = 0; i < cnt; i++){
scanf("%d", p+i);//p+i == &p[i]
}
printf("数组为:\n");
for (i = 0; i < cnt; i++) {
printf("%d\t", *(p+i));//*(p+i) == p[i]
}
printf("\n");
free(p);//不要忘记释放动态内存
p = NULL;//释放后立刻将p指向空
return 0;
}
注意:sizeof可以求出整个数组在内存中所占的字节数,即可以求出数组的长度。但是对动态数组而言这么做是行不通的,因为动态数组是通过指针变量引用的,而对指针变量使用sizeof结果都是4,所以无法通过sizeof求出整个动态数组的长度。不过动态数组的长度可以是变量,这个变量就是数组的长度。
5.5、动态数组长度的扩充和缩小
动态数组在运行的过程可以使用"realloc"函数动态的扩充和缩小长度。
realloc一般形式:
realloc(void *p,unsigned long size)
不常用,就不讲了。
5.6、动态内存和静态内存小结
静态内存是存放在栈中,先进后出;动态内存是采用的堆排序。栈是存储结构,堆不是,静态内存由系统释放,动态内存由程序员自己释放,必须释放,否则会造成内存泄漏
第六节、多级指针
多级指针就是指针的指针的指针.....
假设定义了一个二级指针:int **q;
q前面有两个*,可以拆分为两个部分,即int *和*q
int *表示q里只能放该类型的变量的地址
*q仅表示q是一个指针变量。
所有的多级指针都氛围两个部分,*q和*q前面的。
#include "stdio.h"
#include "stdlib.h"
int main()
{
int i = 20;
int *q = &i;
int **p = &q; //因为这种变量q的基类型是int,所以&q的基类型是int *型。如果定义一个能指向int *型变量的指针变量,有两个要求:首先它是指针变量,即一个*,其次该指针变量指向的是int*型的数据(存放int*型的地址),所以就是int**。几个*就是几级指针。
int ***r = &p;
printf("i = %d\n", ***r);
return 0;
}
结果:20
第七节、指针数组
指针数组的重点是数组,即存放指针的数组。
例:int* arr[5];---整型指针的数组
这里不做细说
第八节、跨函数使用动态内存
跨函数使用动态内存就是指如何在主调函数中使用被调函数中动态分配的内存。下面写一个程序:
#include "stdio.h"
#include "stdlib.h"
void DynamicArray(int **q);//函数声明
int main()
{
int *p = NULL;
DynamicArray(&p); //函数调用
printf("*p = %d\n", *p);
return 0;
}
void DynamicArray(int **q){
*q = malloc(sizeof(*q));
**q = 5;
return;
}
从上可知,动态内存并不会随着函数调用的结束而释放。
为什么传入&p?
第九节、指针和二维数组
记住怎么取就可以了。没必要深究。
取元素:p+i*烈数+j
第十节、函数指针
用的不多,但一定要认识。
存放函数的地址的指针变量就叫函数指针
例:int(*p)(int,int);----定义了一个指针变量p,返回值为int型,且有两个整型参数。p的类型为int(*)(int,int)。
一般形式:函数返回值类型(* 指针变量名)(函数参数列表);
注意:所有的括号都不能省略。函数指针没有自加和自减运算。
以下程序可以自己运行一下看看结果。
#include "stdio.h"
int Max(int,int);
int main()
{
int(*p)(int, int);
int a, b, c;
p = Max;
printf("请输入a和b:");
scanf("%d%d", &a, &b);
c = (*p)(a, b);
printf("a = %d\nb = %d\nmax = %d\n", a, b, c );
return 0;
}
int Max(int x,int y){
int z;
if (x > y) {
z = x;
}else {
z = y;
}
return z;
}
本章到此结束,关于指针并没有完全讲完,会在后续的知识点慢慢填充