1、数组
1.1 概念
数组是若干有限个相同类型的变量在连续内存中有序存储的集合。若将有限个类型相同的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量。用于区分数组的各个元素的数字编号称为下标。
数组定义的一般形式为:
类型说明符 数组名 [常量表达式];
其中,类型说明符是任一种基本数据类型或构造数据类型。数组名是用户定义的数组标识符。方括号中的常量表达式表示数据元素的个数,也称为数组的长度。例如:
int data[10]; //定义了一个整型的数组,data是数组名,10是元素个数,每个元素的类型都是int类型。
注:数组定义时,[]中要给一个常量才可以,可以直接用常量,或者使用宏定义,但不能使用变量。
1.2 数组的分类
按照元素类型分为以下几类:
- 字符数组:char data[5];
- 短整型数组:short data[5];
- 整型数组:int data[5];
- 长整型数组:long data[5];
- 浮点型数组:float data[5]; double data[5];
- 指针型数组:int * data[5];
- 结构体:struct stu f_data[5];
按照维数分为三类,其中最常用的是前两类:
- 一维数组:int data[5];
- 二维数组:int data[5][6];
- 多维数组:int data[4][2][3];
1.3 定义及初始化
1.3.1 一维数组的初始化
一维数组的初始化在定义的时候可以全部初始化,也可以局部初始化。
一维字符数组可以用单个字符进行初始化,也可以采用字符串常量直接复制。当使用字符串赋值时,编译器会自动在字符串的最后增加一个'\0'。
如下图初始化是错误的
1.3.2 二维数组的初始化
二维数组的初始化在定义的时候可以按行初始化,也可以单个初始化。
注意:花括号中的一个花括号代表一个一维数组的初始化。当里面无花括号分组时,按照顺序从第一个开始逐个进行初始化。余下的未赋值的元素用0初始化。二维数组本质上也是一维数组,只不过内部元素放的是一维数组。
1.4 数组元素在内存中的存储
数组元素在内存中连续存放的,且随着数组下标的增长,地址是由低到高变化的。二维数组按照多个一维数组进行存储。
仔细观察输出的结果,可知随着数组下标的增长,元素的地址也在有规律的递增,每次递增量就是元素的内存大小。由此可以得出结论:数组在内存中是线性连续并且递增存放的。
注意:
- sizeof()操作符用于取长度,以字节为单位。sizeof(数组名)即求的是整个数组的大小。sizeof(首元素)即求数组单个元素大小。用0下标,是因为数组至少存在一个有效元素,所以0下标永远存在。
- 数组是使用下标来访问的,下标是从0开始。数组的大小可以通过计算得到。建议采用sizeof(arr)/sizeof(arr[0])这种方式。
1.5 数组元素的引用
一维数组元素的引用方法为
数组名[下标];//下标代表数组元素在数组中的位置,从0开始
二维数组元素的引用方法为
数组名[行下标][列下标]; //行下标和列下标都是从0开始
1.6 结论
- 数组是具有相同类型的集合,数组的大小(即所占字节数)由元素个数乘以单个元素的大小。
- 数组只能够整体初始化,使用循环从第一个逐个遍历赋值。当花括号中用于初始化值的个数不足数组元素大小时,数组剩下的元素依次用0初始化。
- 初始化时,数组的维度或元素个数可忽略,编译器会根据花括号中元素个数初始化数组元素的个数。
- 字符型数组在计算机内部用的时对应的ascii码值进行存储的
2、指针
2.1 概念
在C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是内存地址,数组,函数等占据存储空间的实体。在同一CPU构架下,不同类型的指针变量所占用的存储单元长度是相同的,与CPU的位数相等。
定义指针变量的一般形式为:
类型说明符* 变量名;
类型说明符表示指针变量所指向变量的数据类型;*表示这是一个指针变量;变量名表示定义的指针变量名,其值是一个地址,例如:
char *p;//表示 p是一个指针变量
char c; //c是一个字符变量
p = &c; //p指向字符变量c的存储位置
指针的定义示例:
int a ; //int类型变量a
int *a ; //int* 变量a
int arr[3]; //arr是包含3个int元素的数组
int (* arr )[3]; //arr是一个指向包含3个int元素的数组的指针变量
struct Student *p_struct; //指向结构体类型的指针
int(*p_func)(int,int); //指向返回类型为int,有2个int形参的函数的指针
int(*p_arr)[3]; //指向含有3个int元素的数组的指针
int** p_pointer; //指向一个整形变量指针的指针
2.2 指针的基本运算
- 取地址运算&:获得指针所指内存的地址。
- 解引用运算*:获得指针所指内存里的数据,或者修改所指内存里的数据。
- 赋值运算:将指针指向有意义的内存地址或者变为空指针。
- 指针加减运算:指针的加减运算是按照指针所指内存的数据类型进行运算的。
2.3 指针之间的赋值
指针赋值和其他类型变量赋值一样,就是将地址的值拷贝给另外一个。指针之间的赋值是一种浅拷贝,是在多个函数之间共享内存数据的高效的方法。
int* p1 = & num;
int* p3 = p1;
//通过指针 p1 、 p3 都可以对内存数据 num 进行读写,如果2个函数分别使用了p1 和p3,那么这2个函数就共享了数据num。
2.4 空指针(NULL指针)
每一个指针类型都有一个特殊的值——“空指针”。空指针与同类型的其他指针值都不同,它“保证与任何对象或函数的指针值都不相等”,也就是说空指针不会指向任何地方,它不是任何对象或函数的地址。简单点说,一个指针不指向任何数据,我们就称之为空指针,空指针用NULL表示。在C语言中NULL就是((void *)0)。
下面代码摘自 stdlib.h:
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
空指针不等同于未初始化的指针,未初始化的指针通常指野指针,可能会造成非法访问内存地址,而空指针它不指向任何对象。
NULL指针的概念非常有用,它给了你一种方法,表示某个特定的指针目前并未指向任何东西。例如,一个用于在某个数组中查找某个特定值的函数可能返回一个指向查找到的数组元素的指针。如果没找到,则返回一个NULL指针。
2.5 void*类型指针
由于void是空类型,因此void*类型的指针只保存了指针的值,而丢失了类型信息,我们不知道他指向的数据是什么类型的,只指定这个数据在内存中的起始地址。void*类型变量可以接受任意类型指针的赋值,并且不需要进行强制类型转换。
例如:
Type a,Type *p=&a;(Type等于char, int, struct, int *…)
void *pv;
pv=p;
void指针最常用于内存管理。最典型的,也是大家最熟知的,就是标准库的memcpy函数。它的原型如下:
void *memcpy(void*dest, const void *src, size_t n);
2.6 野指针
野指针是指针指向的内存地址是不可知的(随机的,不正确的,没有明确限制的),这种指针在嵌入式开发中危害特别大。
产生的原因可能是:
- 指针未初始化就使用;
- 指针越界,例如通过指针形式访问数组越界;
- 指针指向的空间已经释放
规避方法:
- 指针初始化;
- 小心指针越界;
- 指针指向空间释放即置NULL;
- 指针使用之前检查有效性。
2.7 带const修饰符的指针
任何变量当其类型限定符为 const 时,则意味着这些变量在使用过程中无法被修改,因此这些变量称为“只读变量”。在算法函数接口中常用const来限定不允许被改变的指针变量。const可以用来限定指针本身的类型,或者是限定指针所指对象的类型,分别对应着指针常量和常量指针。
2.8 数组和指针
数组和指针本质上都代表一块内存,数组名代表这块内存的首地址,而指针存储的是其它变量在内存中的地址。
2.8.1 区别
- 定义时机不同:数组在编译时就已经被确定下来,而指针直到运行时才能被真正的确定到底指向何方。
- 访问方式不同:由于数组名直接代表其内存地址,而指针需要通过读取保存的地址才能读取相应数据,所以数组为直接访问方式,而指针为间接访问方式。
- 支持的运算不同:数组名是数组的首地址,即它本身就是一个(固定的)地址。因此数组名不能进行++,--等运算。而指针可以进行自加或者自减运算。
2.8.2 联系
- 访问数组元素的时候,可以通过使用指针读取,也可以使用数组下标法访问。
- 数组在作为函数参数时会退化为指针,比如在函数的参数中,如果传递的是一个数组,可以用一个指针变量来接收。
2.9 结构体和指针
结构体指针有特殊的语法:-> 符号。
如果p是一个结构体指针,则可以使用 p ->[成员]的方法访问或者修改结构体的成员变量。
2.10 函数和指针
2.10.1 函数的参数和指针
C语言中函数的实参传递给形参是按值传递的,也就是说函数中的形参只拷贝了实参的值,但是实参和形参是不同的内存地址。这种数据传递是单向的,即从调用函数传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。
有时候我们可以使用函数的返回值来回传数据,在简单的情况下是可以的。但是如果返回值有其它用途(例如返回函数的执行状态),或者要回传的数据不止一个,返回值就解决不了了。此时我们通过传递变量的指针解决上述问题。
void change(int* pa)
{
(*pa)++;
//因为传递的是age的地址,因此pa指向内存数据age。
//当在函数中对指针pa解地址时,会直接去内存中找到age这个数据,然后把它增1。
}
int main(void)
{
int age = 19;
change(&age);
printf("age = %d",age);
// age = 20
return 0;
}
再来一个老生常谈的例子:用函数实现交换2个变量交换的功能:
#include<stdio.h>
void swap_bad(int a,int b);
void swap_ok(int*pa,int*pb);
int main(){
int a = 5;
int b = 3;
swap_bad(a,b); //Can`t swap;
swap_ok(&a,&b); //OK
return 0;
}
//错误的写法void swap_bad(int a,int b)
{
int t;
t=a;
a=b;
b=t;
}
//正确的写法:通过指针void swap_ok(int*pa,int*pb)
{
int t;
t=*pa;
*pa=*pb;
*pb=t;
}
有的时候,我们通过指针传递数据给函数不是为了在函数中改变他指向的对象。相反,我们防止这个目标数据被改变。传递指针只是为了避免拷贝大型数据。
例如一个结构体类型Student,我们通过show函数输出Student变量的数据。
typedef struct
{
char name[31];
int age;
float score;
}Student;
//打印Student变量信息void show(const Student * ps)
{
printf("name:%s,age:%d,score:%.2f",ps->name,ps->age,ps->score);
}
在show函数为了防止Student变量的信息改变,采用const修饰符来约束变量。另外我们为什么要使用指针而不是直接传递Student变量呢?从结构体的定义来看,Student变量的大小至少是39个字节,那么通过函数直接传递变量,实参赋值数据给形参需要拷贝至少39个字节的数据。而传递变量的指针却快很多,因为在同一个平台下,无论什么类型的指针大小都是固定的:X86指针4字节,X64指针8字节,远远比一个Student结构体变量小。
2.10.2 函数指针
如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址,函数名表示的就是这个地址。既然是地址,那么就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。
函数指针的定义方式为:
函数返回值类型 (* 指针变量名) (函数参数列表);
“函数返回值类型”表示该指针变量可以指向具有什么返回值类型的函数;“函数参数列表”表示该指针变量可以指向具有什么参数列表的函数。这个参数列表中只需要写函数的参数类型即可。
我们看到函数指针的定义就是将“函数声明”中的“函数名”改成“(*指针变量名)”。但是这里需要注意的是:“(*指针变量名)”两端的括号不能省略,括如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。
函数指针最常用的场合就是回调函数。回调函数是作为参数传给另一个函数的函数,它允许用户把需要调用的函数的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
如下例子:
#include <stdio.h>
//Calculate用于计算积分。一共三个参数。
//第一个为函数指针func,指向待积分函数。
//二三参数为积分上下限
//定义实现回调函数的"调用函数"
double Calculate(double(*func)(double x), double a, double b)
{
double dx = 0.0001;//细分的区间长度
double sum = 0;
for (double xi = a+dx; xi <= b; xi+=dx)
{
double area = func(xi)*dx;
sum +=area;
}
return sum;
}
//定义回调函数
double func_1(double x)
{
return x*x;
}
//定义回调函数
double func_2(double x)
{
return x*x*x;
}
void main()
{
//实现函数回调
printf("%lf\n", Calculate(func_1, 0, 1));
printf("%lf\n", Calculate(func_2, 0, 1));
}
在上面例子中,更常用typedef来定义函数指针:
比如:typedef double(*pFunc)(double x);//表示定义函数指针pFunc代表double(*)(double x)这样的函数。
double Calculate(double(*func)(double x), double a, double b)就可以改为:
double Calculate(pFunc, double a, double b)
用typedef来定义的好处,就是可以使用一个简短的名称来表示一种类型,而不需要总是使用很长的代码来,这样不仅使得代码更加简洁易读,更是避免了代码敲写容易出错的问题。