1. C 数组基本概念
C 语言支持数组数据结构,它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。数组的声明并不是声明一个个单独的变量,比如 runoob0、runoob1、...、runoob99,而是声明一个数组变量,比如 runoob,然后使用 runoob[0]、runoob[1]、...、runoob[99] 来代表一个个单独的变量。所有的数组都是由连续的内存位置组成。最低的地址对应第一个元素,最高的地址对应最后一个元素。总结下来:
(1)数组是相同类型的变量的有序集合
(2)数组在一片连续的内存空间中存储元素
(3)数组元素的个数可以显示或隐式指定
1.1. 声明数组
在 C 中要声明一个数组,需要指定元素的类型和元素的数量,如下所示:type arrayName [ arraySize ]; 这叫做一维数组。arraySize 必须是一个大于零的整数常量,type 可以是任意有效的 C 数据类型。例如,要声明一个类型为 double 的包含 10 个元素的数组 balance,声明语句如下:double balance[10];
1.2. 初始化数组
在 C 中,您可以逐个初始化数组,也可以使用一个初始化语句,如下所示:double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};
1.3. 访问数组元素
数组元素可以通过数组名称加索引进行访问。元素的索引是放在方括号内,跟在数组名称的后边。例如:double salary = balance[9];
2. 数组地址(&a)与数组名a
(1)数组名a代表数组首元素的地址。因此,第2个元素的地址为a+1,以此类推……。注意a或a+i表示元素的地址。可以用*(a+i)取出元素的值,也可以用a[i]来取出元素的值,因为当编译中遇到a[i]会自动转为*(a+i)。反过来也可知,第1个元素的地址为a或&a[0],第2个元素的地址为a+1或&a[1],第i个元素的地址为(a+i)或&a[i]……
(2)数组的地址需要用取地址符&才能得到。即形如&a取的是整个数组的地址,所以&a+1表示指向整个数组的最后面的位置。
(3)数组的首元素的地址值与数组的地址值相同,但是两个不同的概念。
【编程实验】数组名和数组地址
#include <stdio.h>
int main(){
//将数组每个元素初始化为0
int a[5] = {0};//含义,将第1个元素初始化为0,其余为0.
printf("a = %p\n",a); //首元素的地址
printf("&a = %p\n",&a); //整个数组的地址,从数值与看,与a一样。
printf("&a[0] = %p\n",&a[0]);//第1个元素的地址
return 0;
}
2.1. 数组名的盲点
(1)数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组。如int a[5]表示a的类型为int[5],所以sizeof(a)表示取整个数组的大小,&a表示数组的地址。
(3)数组名的外延:除了sizeof(a)和&a外,数组名经常可看作是一个常量指针。但要注意这里仅仅是“看作”,而不是真正的指针。不同于指针,数组名只是编译过程中的一个符号,编译器并不为其分配内存,有人称之为“伪变量”。因此,形式a++\a—或a=b(其中b是另一个数组名)这些都是错误的,因为a只是一个符号,编译器会把数组信息(如大小,地址)放入符号表中,每次遇到数组名a时,就会从符号表中取出这个数组的地址,然后用这个固定的地址代替 a,所以这个符号并没有被分配内存空间,而上述操作都是针对变量而言的,故数组名只能做为右值使用。
(4)对数组的引用,如a[i]或*(a+i),只需访问内存一次,而指针的引用如*(p+i)则需要两次,首选通过&p找到p指针,然后加i,再从p+i里面取出的内容。
(5)当数组名作为形参时,将退化为指针。即可以把数组名当成指针来用,这里的sizeof(数组名)为4,即指针的长度。
【实例分析】数组和指针并不相同
#include <stdio.h>
int main(){
//将数组每个元素初始化为0
int a[5] = {0};
int b[2];
int* p = NULL;
p = a;
printf("a = %p\n",a); //首元素的地址
printf("p = %p\n",p); //p==a。
printf("&p = %p\n",&p);//指针p的地址
printf("sizeof(a) = %d\n",sizeof(a));//数组的大小:20
printf("sizeof(p) = %d\n",sizeof(p));//指针的大小为4.
printf("\n");
p = b;
printf("b = %p\n",b); //首元素的地址
printf("p = %p\n",p); //p==b。
printf("&p = %p\n",&p);//指针p的地址
printf("sizeof(b) = %d\n",sizeof(b));//数组的大小:8
printf("sizeof(p) = %d\n",sizeof(p));//指针的大小为4.
//a = b; //编译错误,数组名不能作为左值;
//a++; //编译错误,数组名被编译一个固定地址,相当于0xaabbccdd++的错误
return 0;
}
2.2. 小结
(1)数组是一片连续的内存空间
(2)数组的地址和数组首元素的地址意义不同
(3)数组名在大多数情况下被当成常量指针处理
(4)数组名其实并不是指针,不能将其等同于指针。
3. 指针
变量的回顾,程序中的变量只是一段存储空间的别名,那么是不是必须通过这个别名才能使用这段内存空间呢?
3.1. *号的意义
*号类似一把钥匙,通过这把钥匙可以打开内存,读取内存中的值。
(1)在指针声明时,*号表示所声明的变量为指针
(2)在指针使用时,*号表示取指针所指向的内存空间中的值。
【实例分析】指针使用示例
#include <stdio.h>
int main()
{
int i = 0;
int* pI;
char* pC;
float* pF;
pI = &i;
*pI = 10;
printf("%p, %p, %d\n", pI, &i, i); //p == &i
printf("%d, %d, %p\n", sizeof(int*), sizeof(pI), &pI);
printf("%d, %d, %p\n", sizeof(char*), sizeof(pC), &pC);
printf("%d, %d, %p\n", sizeof(float*), sizeof(pF), &pF);
return 0;
}
3.2. 传值调用与传址调用
(1)指针是变量,因此可以声明指针参数
(2)当一个函数体内部需要改变实参的值,则需要使用指针参数
(3)函数调用时,实参值将复制到形参
(4)指针适用于复杂数据类型作为参数的函数中
【编程实验】利用指针交换变量
#include <stdio.h>
int swap(int* a, int* b)
{
int c = *a;
*a = *b;
*b = c;
}
int main()
{
int aa = 1;
int bb = 2;
printf("aa = %d, bb = %d\n", aa, bb);
swap(&aa, &bb);
printf("aa = %d, bb = %d\n", aa, bb);
return 0;
}
3.3. 常量与指针
常量指针
定义: 又叫常指针,可以理解为常量的指针,也即这个是指针,但指向的是个常量,这个常量是指针的值(地址),而不是地址指向的值。
关键点:
- 1.常量指针指向的对象不能通过这个指针来修改,可是仍然可以通过原来的声明修改;
- 2.常量指针可以被赋值为变量的地址,之所以叫常量指针,是限制了通过这个指针修改变量的值;
- 3.指针还可以指向别处,因为指针本身只是个变量,可以指向任意地址;
代码形式:
int const* p; const int* p;
指针常量
定义:本质是一个常量,而用指针修饰它。指针常量的值是指针,这个值因为是常量,所以不能被赋值。
关键点:
- 1.它是个常量!
- 2.指针所保存的地址可以改变,然而指针所指向的值却不可以改变;
- 3.指针本身是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化;
-
代码形式:
int* const p;
指向常量的常指针
定义:指向常量的指针常量就是一个常量,且它指向的对象也是一个常量。
关键点:
代码形式:
const int* const p;
-
那如何区分这几类呢? 带两个const的肯定是指向常量的常指针,很容易理解,主要是如何区分常量指针和指针常量:
- 1.一个指针常量,指向的是一个指针对象;
- 2.它指向的指针对象且是一个常量,即它指向的对象不能变化;
(1)几种情况
①const int* p; //p可变,p指向的内容不可变
②int const* p; //p可变,p指向的内容不可变
③int* const p; //p不可变,p指向的内容可变
④const int* const p; //p不可变,p指向的内容不可变
(2)口诀:左数右指
①当const出现在*号的左边时,指针指向的数据为常量
②当const出现在*号的右边时,指针本身为常量
【实例分析】常量与指针
#include <stdio.h>
int main()
{
int i = 0;
const int* p1 = &i;
int const* p2 = &i;
int* const p3 = &i;
const int* const p4 = &i;
*p1 = 1; // compile error
p1 = NULL; // ok
*p2 = 2; // compile error
p2 = NULL; // ok
*p3 = 3; // ok
p3 = NULL; // compile error
*p4 = 4; // compile error
p4 = NULL; // compile error
return 0;
}
在实际应用中,常量指针要比指针常量用的多,比如常量指针经常用在函数传参中,以避免函数内部修改内容。
size_t strlen(const char* src); //常量指针,src的值不可改变; char a[] = "hello"; char b[] = "world"; size_t a1 = strlen(a); size_t b1 = strlen(b);
虽然a、b是可以修改的,但是可以保证在strlen函数内部不会修改a、b的内容。
3.4. 空指针、野指针
既然讲到了指针,那顺便说一下空指针、野指针的问题。
空指针就是保存地址为空的指针,使用指针时必须先判断是否空指针,很多问题都是这一步导致的。
野指针是在delete掉指针之后,没有置0,导致指针随意指向了一个内存地址,如果继续使用,会造成不可预知的内存错误。
另外指针的误用很容易造成BUG或者内存泄漏。
看代码:
//-------空指针-------// int *p4 = NULL; //printf("%d",*p4); //运行Error,使用指针时必须先判断是否空指针 //-------野指针(悬浮、迷途指针)-------// int *p5 = new int(5); delete p5; p5 = NULL; //一定要有这一步 printf("%d",*p5); //隐藏bug,delete掉指针后一定要置0,不然指针指向位置不可控,运行中可导致系统挂掉 //-------指针的内存泄漏-------// int *p6 = new int(6); p6 = new int(7); //p6原本指向的那块内存尚未释放,结果p6又指向了别处,原来new的内存无法访问,也无法delete了,造成memory leak
3.5. 小结
(1)指针是C语言中一种特别的变量
(2)指针所保存的值是内存的地址
(3)可以通过指针修改内存中的任意地址内容
4. 函数指针分析
4.1. 函数类型
(1)C语言中的函数有自己特定的类型,这个类型由返回值、参数类型和参数个数共同决定。如int add(int i,int j)的类型为int(int,int)。
(2)C语言中通过typedef为函数类型重命名
typedef type name(parameter list);//如typedef int f(int,int);
4.2. 函数指针
(1)函数指针用于指向一个函数,函数名是执行函数体的入口地址。
(2)定义函数指针的两种方法
①通过函数类型定义:FuncType* pointer;
②直接定义:type(*pointer)(parameter list);
//其中type为返回值类型,pointer为函数指针变量名,parameter list为参数类型列表
【实例分析】函数指针的使用(技巧:使用函数指针直接跳转到某个固定的地址开始执行)
#include <stdio.h>
typedef int (FUNC)(int);
int test(int i)
{
return i * i;
}
void f()
{
printf("Call f()...\n");
}
int main()
{
FUNC* pt = test; //合法,函数名就是函数体的入口地址
//直接定义函数指针,&f是旧式写法。函数名只是一个符号(不是变量),
//与数组名一样,并不为其分配内存,因此&f和f在数值上是相等的。
void(*pf)() = &f; //如果知道某个函数的地址,这里可以改为一个固定的地址值,实现跳转!
printf("pf = %p\n",pf);
printf("f = %p\n",f);
printf("&f = %p\n",&f); //结果应为:pf == f == &f;
pf();//利用函数指针调用
(*pf)(); //旧式写法
printf("Function pointer call:%d\n",pt(2));
return 0;
}
4.3. 回调函数
(1)回调函数是利用函数指针实现的一种调用机制
(2)回调机制原理
①调用者不知道具体事件发生时需要调用的具体函数
②被调函数不知道何时被调用,只知道需要完成的任务
③当具体事件发生时,调用者通过函数指针调用具体函数。
(3)回调机制中的调用者和被调用者互不依赖。
【实例分析】回调函数使用示例
#include <stdio.h>
typedef int (*Weapon)(int); //操作某种武器的函数
//使用某种武器与boss进行战斗
void fight(Weapon wp,int arg) //arg为传给函数指针的参数
{
int result = 0;
printf("Fight boss!\n");
result = wp(arg);//调用回调函数,并传入参数arg
printf("Boss loss:%d\n",result);//Boss失血多少?
}
//使用武器——刀
int knife(int n)
{
int ret = 0;
int i = 0;
for (i=0; i< n; i++)
{
printf("Knife attack:%d\n",1);
ret++;
}
printf("\n");
return ret;
}
//使用武器——剑
int sword(int n)
{
int ret = 0;
int i = 0;
for (i=0; i< n; i++)
{
printf("Sword attack:%d\n",5);
ret++;
}
printf("\n");
return ret;
}
//使用武器——枪
int gun(int n)
{
int ret = 0;
int i = 0;
for (i=0; i< n; i++)
{
printf("Gun attack:%d\n",10);
ret++;
}
printf("\n");
return ret;
}
int main()
{
fight(knife, 3);//用刀砍3次
fight(sword, 4);//用剑刺4次
fight(gun, 5); //开枪5次
return 0;
}
4.4. 小结
(1)C语言中的函数都有特定的类型
(2)可以使用函数类型定义函数指针
(3)函数指针是实现回调机制的关键技术,同时函数指针也是C语言面向对象结构体抽象的关键技术
(4)通过函数指针可以在C程序中实现固定地址跳转