在Ubunutu上学习C语言(二):数组和指针
数组和指针
数组
基本概念
逻辑: 一次性定义多个相同类型的变量,并且给他分配一片连续的内存
int arr[5];
//1、向系统申请一片连续的内存,名字为arr
//2、[5]告诉系统这一片连续的内存需要分成5等分
//3、每一个等份都用来存放一个int数据,4字节
初始化
只有在定义的时候赋值, 才可以称为初始化。数组只有在初始化的时候才可以统一赋
值。
int arr [5] = {1,2,3,4,5} ; // 定义并初始化数组int arr [5] = {1,2,3} ; //可以, 不完全初始化int arr [5] = {1,2,3,4,5,6,7,8,9} ; // 错误(但是可以用) , 越界初始化, 越界部分将会被编译器舍弃int arr [ ] = {1,2,3,4,5,6,7,8,9} ; // 可以, 用户没有直接给定数组大小,
// 但是有初始化, 因此数组的大小会在初始化时确定, 大小为 9int arr [ ] ; // 错误的, 没有给定大小也没有初始化, 因此数组的内存大小无法确定系统无法分配
//初始化越界
int arr[5] = {1,2,3,4,5,6,7,8,9};
for(size)
所以注意:
- 数组在定义时就必须确定他的大小;
- []里必须有数组的大小,如果没有就必须初始化
数组元素引用
存储模式:一片连续的内存,按照数据的类型进行分割成若干个大小相同的格子。
元素的下标与偏移量:以数组开头为基础的偏移的量(数据类型大小)
int arr [5] = {1,2,3,4,5} ; // 定义并初始化数组printf("arr[0]:%d\n" , arr[0]);arr[0] = 99 ; // 把数组的第1个元素( 偏移量为0 )修改为 99
printf("arr[0]:%d\n" , arr[0]);arr[5] = 250 ; //"错误" 越界访问, 并很有可能造成非法访问
printf("arr[5]:%d\n" , arr[5]);arr = {9,8,7,6,5,4}; // 整体赋值只允许在初始化中
arr = 100 ; // 错误printf("sizeof(arr):%ld\n" , sizeof(arr));
//sizeof 是C语言中的一个单目运算符,用于计算数据类型或表达式在内存中所占的字节数。
int len = sizeof(arr) / sizeof(int) ; // 求数组元素的个数for (size_t i = 0; i < len ; i++)
{printf("arr[%ld]:%d\n" , i , arr[i]);
}
字符数组
概念: 专门用来存放字符类型数据的数组, 称为字符数组
初始化和引用
char ch1 [5] = {'H','e','l','l','o'} ; // 定义一个字符类型的数组并把'H','e','l','l','o' 一个一个存进去char ch2 [6] = {"Hello"} ; // 定义一个字符型的数组, 并把 "Hello" 字符串存放到数组中 ,因此该数组为字符串数组char ch3 [6] = "Hello" ; // 与ch2 一样, 大括号可以省略ch3[1] = 'E' ; // 可以, 把数组中第二个元素‘e’修改为‘E’
ch3 = "Even" ; // 不可以, 只有在初始化的时候才能整体赋值printf("%s\n" , ch1); // 在访问ch1的时候并没有发现结束符,因此很有可能会把ch2的内容一并输出
//ch1 为字符数组, 它没有结束符,因此在打印输出的时候应该避免使用 %s 进行输出, 有可能会造成越界访问。
//如果想安全输出“Hello”,必须在末尾补上`\0`
char ch1[6] = {'H','e','l','l','o','\0'};
printf("%s\n", ch1);//或者用字符串初始化,编辑器会自动补上`\0`
char ch1[] = "Hello";
//如果输入数字就是ascii码
char buf[5] = {1,2,3,4,5};
//放入内存的是字符1,2,3,4,5对应的ASCLL码
数组名的意义
数组名在不同的场合下的含义有两种:
表示整个数组的首地址:
在数组定义的时候
在使用sizeof 运算符中 数组名表示整个数组的首地址(求得整个数组的大小)
在取地址符 中 &arr , arr 表示整个数组
表示整个数组首元素的首地址:
- 其它情况
int arr[10] ; // arr 表示整个数组的首地址
int len = sizeof arr ; // arr 表示整个数组的首地址 , sizeof 运算符后的括号可以省略
int (* p) [10] = &arr; //arr 表示整个数组的首地址int *p1 = arr ; // arr 表示数组的首元素的首地址
数组下标
数组的下标实际上只是编译器提供一种简写,实际上如下:
int a [100] ;
a[10] = 250 ; ====> *(a+10) = 250 ;
通过加法交换律,有如下结果:
a[10] = 250 ;
*(a+10) = 250 ;
*(10+a) = 250 ;
10[a] = 250 ;
字符串常量
字符串常量是一个被存放在常量区的字符串,实际上也可称为一个匿名数组。
匿名数组,同样满足数组名的含义。
char * msg2 = "Hello Even" ;// "Hello Even" 字符串常量首元素的首地址
char * msg1 = "Hello Even"+1 ;printf("%s\n", "Hello Even" ) ;// "Hello Even" 字符串常量首元素的首地址
printf("%s\n", &"Hello Even" ) ; // "Hello Even" 字符串常量的整个数组的地址printf("%c\n", "Hello Even"[6] ) ; // "Hello Even" 字符串常量首元素的首地址[6]
// [6] 相当于+6个单位(char) 得到 ‘E’

零长数组:(预习结构体)
概念: 数组的长度为0 , char arr [0] ;
用途:一般会放在结构体的末尾, 作为可变长度的入口。(数组是唯一一个允许越界访问的载体)
struct node
{
int a;
char b ;
float c ;
..
...
int len ;
char arr[0] ;
}struct node *p = malloc(sizeof(struct node) + 20 ); // + 20 就是在原有的基础上增加20字节
p‐>len = 20 ; // 设置额外增长的长度为 20

变长数组
概念: 定义是, 使用一个变量作为数组的长度(元素的个数)。
重点: 变长数组并不是说在任意时候他的长度可以随意变化, 实际上只是在定义之前数组的长度是未知的有一个变量来决定, 但是定义语句过后变长数组的长度由定义那一刻变量的大小来决定
int a = 200 ; // a 作为一个普通的变量 , 200 则可以作为arr 的长度
a = 99 ; // 99 可以作为 arr 的长度int arr[a]; // a 当前是 99 , 因此数组arr 的长度已经确定为 99
//从此以后该数组的长度已经确定为99 不会再变换
a = 10 ; // a = 10 并不会影响数组的长度
注意:
- 因为数组的长度未确定, 因此它不允许初始化。
- 在使用的时候可以通过该变长数组来有限的节省内存空间。
多维数组
概念: 数组内部的成员也是数组
int a [2][3] ;
定义与初始化
int arr[2][3] = { {1,2,3} , { 4,5,6} };
int arr1[2][3] = { 1,2,3,4,5,6};
arr[0] [0] = 100 ; // 数组:(通过下标来访问)
*(*(arr+0)+0) = 100 ; // 通过指针偏移来访问
int arr[2][3] = { {1,2,3} , { 4,5,6} };
int arr1[2][3] = { 1,2,3,4,5,6};int *p = arr ; // p指向数组arr 的首元素for (int i = 0; i < 2; i++){for (int j = 0; j < 3; j++){printf("arr[%d][%d]:%d\t" ,i ,j , arr[i][j] );}
}printf("\n");for (int i = 0; i < 6 ; i++){ // *arr 得到元素1 的地址 + 1 则是加一个 int 类型printf("*(*(arr+%d)):%d\t" , i,*(*(arr)+i) );
}printf("\n");for (int i = 0; i < 6 ; i++)
{ // arr 指的是首元素的首地址 {1,2,3} 的首地址 + 1则 + 3个整型printf("*(*(arr+%d)):%d\t" , i,*(*(arr+i)) );
}for (int i = 0; i < 6 ; i++){ // p 只是一个普通的整型指针, 与二维数组没有任何的关系printf("*(p+%d):%d\n" , i ,*(p+i));
}
指针
内存地址
字节: 字节是内存容量的一个单位, byte , 一个字节byte 有 8个位 bit
地址: 系统位了方便区分每一个字节的数据, 而对内存进行了逐一编号, 而该编号就是内存地址。

基地址
单字节的数据: char 它所在地址的编号就是该数据的地址
多字节的数据:int 它拥有4个连续的地址的编号 , 其中地址值最小的称为该变量的地址

取地址符号
每一个变量其实都对应了片内存, 因此都可以通过 & 取地址符号把其地址获得。
int a ;
char c ;printf("a的地址为:%p\n" , &a);
printf("c的地址为:%p\n" , &c);运行结果:
a的地址为:0x7fffd5595d84
c的地址为:0x7fffd5595d83
%p就是输出地址的符号

注意:
-
虽然不同的数据类型所占用的内存空间不同, 但是他们的地址所占用的内存空间(地址的大小= 指针的大小)是恒定的, 由系统的位数来决定 32位 / 64位
-
不同的地址他从表面上看似乎没有什么差别,但是由他们所代表的内存的尺寸是不一样的(由内存中所存放的数据类型相关),因此我们在访问这些地址的时候需要严格区分它们的逻辑关系。
指针的概念
&a 就是a的地址 , 实质上也可以理解为他是一个指针 指向 a的地址。
专门用来存放地址的一个变量,因此指针的大小是恒定的 ,由系统的位数来决定。
指针的定义
int a ; // 定义一片内存名字叫 a , 约定好该内存用来存放 整型数据
int * p ; // 定义一片内存名字叫 p , 约定好该内存用来存放 整型数据的地址
char * p1 ; // 定义一片内存名字叫 p1 , 约定好该内存用来存放 字符数据的地址
double * p2 ; // 定义一片内存名字叫 p2 , 约定好该内存用来存放 双精度数据的地址
指针的类型,并不是用来决定该指针的大小,而是用来告诉编译器如果我们通过来指针来访问内存时需要访问的内存的大小尺寸。
指针的赋值
int a = 100 ;
int * p = &a ; // 定义并初始化double d = 1024.1234 ;
double * p1 = &d ;// 定义并初始化float f ;
float * p2 ;// 定义并没有初始化p2 = &f ; // 给指针赋值
不同类型的指针,应该用来指向与其相对应的类型的变量的地址。
指针的索引
通过指针获得它所指向的数据(解引用/取目标)
int a = 100 ;
int * p = &a ;*p = 250 ; // *p <==> aprintf("*p:%d\n" , *p) ;
野指针
概念: 指向一块未知内存的指针, 被称为野指针。
int * p;
危害:
引用野指针的时候,很大概率我们会访问到一个非法内存,通常会出现段错误(Segmentation fault (core dumped))并导致程序崩溃。
更加严重的后果,如果访问的时系统关键的数据,则有可能造成系统崩溃
产生原因:
定义时没有对他进行初始化
指向的内存被释放,系统已经回收, 后该指针并没有重新初始化
指针越界
如何防止:
定义时记得对他进行初始化
绝对不去访问被回收的内存地址, 当我们释放之后应该重新初始化该指针。
确认所申请的内存的大小,谨防越界
空指针
在很多的情况下我们一开始还不确定一个指针需要指向哪里,因此可以让该指针先指向一个不会破坏系统关键数据的位置, 而这个位置一般就是NULL (空)。因此指向该地址的指针都称之为空指针。

空指针就是保存了地址值为零的一个地址, 也就零地址NULL
int * p1 = NULL ; // 定义一个指针, 并初始化为空指针( 指向 NULL )*p1 = 250 ; // 段错误 , 该地址不允许写入任何东西printf("%p ‐‐ %d \n" , NULL , NULL ); // (nul‐‐0)
指针运算
指针的加法:意味着地址向上移动若干个目标 (指针的类型)
指针的减法:意味着地址向下移动若干个目标 (指针的类型)
long l ;
long *p = &l ;int i ;
int * p1 = &i;printf("p:%p\n" , p );
printf("p+1:%p\n" , p+1 );printf("p1:%p\n" , p1 );
printf("p1+1:%p\n" , p1+1 );运行结果:
p:0x7fffcb862610
p+1:0x7fffcb862618
p1:0x7fffcb86260c
p1+1:0x7fffcb862610

指针在加减运算的过程中, 加/减的大小取决于该指针他自己的类型,与它所执行的数据实际的类型没有关系。
char型指针
从本质上来看,字符指针其实就是一个指针而已, 只不过该指针用来指向一个字符串/字符串数组。
char * msg = "Hello Even" ;
多级指针
如果一个指针p1 它所指向的是一个普通变量的地址,那么p1就是一个一级指针
如果一个指针p2 它所指向的是一个指针变量的地址,那么p2就是一个二级指针
如果一个指针p3 它所指向的是一个指向二级指针变量的地址,那么p3就是一个三级指针
int a = 100 ;
int * p1 = &a ; // 那么p1就是一个一级指针
int ** p2 = &p1 ; // 那么p2就是一个二级指针
int *** p3 = &p2 ; // 那么p3就是一个三级指针

指针的万能拆解方法
对于任何的指针都可以分为两部分:
第一部分: 说明他是一个指针 (*p)
第二部分: 说用它所指向的内容的类型 (*p)以外的东西
char * p1 ; // 第一部分: * p1 , 第二部分 char 说明p1 指向的类型为char
char **p2 ; // 第一部分: * p2 ,第二部分 char * 说明p2 指向的类型为char *
int **p3 ; // 第一部分: * p3 ,第二部分 int * 说明p3 指向的类型为int *
char (*p4) [3] ; // 第一部分: * p4 , 第二部分 char [3] , 说明p4 指向一个拥有3个元素的char 数组
char (*p5) (int , float) ; // 第一部分: * p5, 第二部分char (int , float), 说明
// 说明该指针指向一个 拥有char类型返回, 并需要 一个int 和 float 参数的函数
void *(*p6) (void *); //第一部分: * p6, 第二部分 void * (void *)
// 说明p6 指向一个 拥有 void * 返回并需要一个void * 参数的函数 函数指针)

总结:
以上指针 p1 p2 p3 p4 p5 p6 本质上都是指针,因此它们的大小都是 8字节(64位系统)
以上指针 p1 p2 p3 p4 p5 p6 本质上都是指针, 唯一的不容是它们所指向的内容的类型不同
void型指针
表示该指针的类型暂时是不确定
要点:
void 类型的指针,是没有办法直接索引目标的。必须先进行强制类型转换。
void 类型指针,无法直接进行加减运算。
void关键字的作用:
修饰指针,表示该指针指向了一个未知类型的数据。
修饰函数的参数列表, 则表示该函数不需要参数。
修饰函数的返回值,则表示该函数没有返回值。
void * p = malloc(4) ; // 使用malloc 来申请 4个字节的内存, 并让p来指向该内存的入口地址*(int *)p = 250 ; // 先使用(int*) 来强调p是一个整型地址 ,然后再解引用
printf("*p:%d\n", *(int*)p);// 输出时也应该使用对应的类型来进行输出
*(float*)p = 3.14 ;
printf("*p:%f\n", *(float*)p);int * a ;
char * b ;
float * f ;
void * k ;
注意:
以上写法 void * p , 在实际开发中不应该出现。以上代码只是为了说明语法问题。
const 指针
const修饰指针有两种效果:
- 常指针 修饰的是指针本身, 表示该指针变量无法修改
char * const p ;char arr [] = "Hello" ; char msg [] = "Even" ; char * const p = arr ;// p = msg ; // p 被const 所修改, //说明P是一个常量 ,他的内容(所指向的地址)无法修改 *(p + 1 ) = 'E' ; // p所指向的内容是可以通过p 来修改 (只要保持P所指向的地址不变即可) printf("%s\n" , p );
- 常目标指针 修饰的是指针所指向的目标,表示无法通过该指针来改变目标的数据
char const * p ; const char * p ; char arr [] = "Hello" ; char msg [] = "Even" ;const char * p1 = arr ; p1 = msg ; // p1 的指向是可以被修改的 // *(p1+1) = 'V' ; // 常目标指针, 不允许通过该指针来它所指向的内容*(msg+1) = 'V' ; // 虽然p1不能修改所指向的内容, 但是内容本身是可以被修改的 printf("%s\n" , p1 );
总结:
常指针并不常见。
常目标指针,在实际开发过程中比较常见,用来限制指针的权限为只读

数组和指针结合
int arr [5] = {1,2,3,4,5} ;
int * p1 = &arr ;
int * p2 = arr ;printf("*p1:%d\n" , *(p1+1)) ;
printf("*p2:%d\n" , *(p2+1)) ;

int arr [5] = {1,2,3,4,5} ;int * p2 = &arr[2] ;printf("*p2‐1:%d\n" , *(p2‐1)) ;//2
printf("*p2:%d\n" , *p2) ;//3
printf("*p2+1:%d\n" , *(p2+1)) ;//4
以上代码是通过指针p2 来访问数组中的元素。
一开始定义p2并初始化让指针p2 指向数组中第3个元素的地址。
当我们使用p2 进行指针加减运算的时候 ,由于指针是整型的,可以访问到数组中的一个元素以及上一个元素
数组指针
数组指针: 专门用来指向一个数组的指针。
int * p ;
int (* p) [5] ; //定义一个 名为p 的指针,
//并且确定他指向的类型为int [5] ,一个拥有5个元素的整型数组int arr [5] = {1,2,3,4,5} ;int (*p) [5] = arr ;printf("arr:%p\n" , &arr );printf("%p‐‐‐%d\n" ,p , (*p)[2] ); // 3 * p ==> arr
printf("%p‐‐‐%d\n" , p+1 , (*(p+1))[2] ); // 已经越界访问
以上代码中 p指向的是 一个整型数组并有5个元素。 因此在对p 进行加减运算时, 是加减一个数组 。
int a = 100 ;
int b = 250 ;
int c = 550 ;
int d = 256 ;
int e = 998 ;int * p [5] = {&a, &b , &c , &d , &e} ; // 定义一个名字为 p的数组, 并且确定该数组中用来存放int * 整型地址for (size_t i = 0; i < 5; i++)
{printf("*p[%d]:%d \n" , i , *(p[i]) ); // p[0] ‐‐》 &a
}
数组和指针理解题
#include <stdio.h>int main(int argc, char const *argv[])
{int (*p)[5] = NULL; //一个数组指针p,指向一个含5个int的数组int arr[5] = {1,2,4,5,7};//一个数组arrp = &arr;//数组指针p指向整个数组arrprintf("arr = %p\n",arr ); //arr数组的首个元素的基地址,就是&arr[0]printf("&arr = %p\n",&arr ); //数组对象的基地址,类型为int*[5]printf("p = %p\n",p); //p指向整个arr数组的地址,等于&arrprintf("*p = %p\n",*p ); //arr数组的基地址,等于arrprintf("p+1 = %p\n",p+1 ); //p数组的基地址再偏移20个字节printf("(*p)+1 = %p\n",(*p)+1); //arr数组的基地址偏移4个字节,就是元素2的地址printf("**p = %p\n",**p); //**p是一个int值,输出1int a = 0x123456 ;printf("%p\n%p\n",a,&a );return 0;
}

#include <stdio.h>int main(void)
{int a[5] = {1,2,3,4,5};//一个数组int b[3][4] = { {250,2,3,4}, {5,6,7,8}, {9,10,11,12}};//双层数组,二维数组printf("&a: %p\n",&a); //整个数组a的地址printf("&a+1: %p\n",&a+1); //整个数组往后加5*4个字节printf("a: %p\n",a); //数组a的首元素的地址printf("a+1: %p\n",a+1); //数组a的第二个元素的地址printf("\n");printf("&b: %p\n",&b); //二维数组b的首地址printf("&b+1: %p\n",&b+1); //整个数组往后加12*4个字节printf("\n");printf("b: %p\n",b); //数组b的首元素{250,2,3,4}的地址printf("b+1: %p\n",b+1); //数组b的第二个元素{5、6、7、8}的地址,也就是增加四个整型printf("\n");printf("*b: %p\n",*b); //b指的是{250,2,3,4}的地址,*b是250的地址printf("*b+1: %p\n",*b+1); //*b就是2的地址printf("\n");printf("**b: %d\n",**b); //**b得到值printf("**b+1: %d\n",**b+1); //**b+1就是值+1printf("**(b+1):%d\n",**(b+2)); //第三个元素的首元素的值,9printf("*(*(b+2)+2):%d\n",*(*(b+2)+2)); //第三个元素的地址偏移两个,就是11的值// 使用指针来访问二维数组的每一个元素for (size_t i = 0; i < 12; i++){printf("%d\t" ,*((*b)+i) );}// 使用数组来访问二维数组for (size_t i = 0; i < 3 ; i++){for (size_t j = 0; j < 4; j++){printf("b[%d][%d]:%d\t" , i , j , b[i][j] );}}// *((*b)+i)return 0;
}

#include <stdio.h>int main(void)
{int arr[] = { 1, 3, 5, 7, 9};int len = sizeof(arr) / sizeof(int); //求数组长度int i;for(size_t i = 0; i < len; i++) {printf("*(arr+%d) = %d\n",i, *(arr+i) ); //数组首元素的地址,往后偏移,再*取值}printf("\n");return 0;
}
#include <stdio.h>int main(void)
{int arr[] = { 1, 3, 5, 7, 9};int i, *p = arr; // p 指向的是首元素的首地址 (p只是一个普普通通的整型指针)int len = sizeof(arr) / sizeof(int);for(i=0; i < len; i++){printf("*(p+%d) = %d\n",i, *(p+i)); //p就是指向首元素的地址}printf("\n");return 0;
}
#include <stdio.h>int main(void)
{int arr[] = { 1, 3, 5, 7, 9};int *p = &arr[2]; // p只是一个普普通通的整型指针) , 并指向的 数据5 的地址// arr[2] 是数据 5 & 则把数据5 的地址进行取出 printf("%d, %d, %d, %d, %d\n", *(p-2), *(p-1), *p, *(p+1), *(p+2) );return 0;
}
#include <stdio.h>int main(void)
{int arr[] = { 1, 3, 5, 7, 9};int i, *p = arr, len = sizeof(arr) / sizeof(int);//p是指向首元素的地址的指针for(i=0; i < len; i++){// * 和 ++ 得优先级是同级的 , 又因为当前这一级的结合性是从右往左 , 因此先 p++ , 但是是后缀所以先预算再加加printf("%d\n", *p++ ); // 先运算得到 1 , 然后再地址+1 printf("%d\n", (*p)++ ); // 先得到p得内容 3 , 然后再对3进行自加for (size_t j = 0; j < len ; j++){printf("arr[%d]:%d\t" , j , arr[j]);} printf("\n");}printf("\n");return 0;
}

#include <stdio.h>int main(void)
{int a = 1, b = 2, c = 3;int *arr[3] = {&a, &b, &c};//整型指针数组int **parr = arr; // 第一部分 *p , 第二部分 int * 说明类型 为指针类型 +1 则+1个指针类型 8字节(64) // arr[0] --> &a *&a --> a printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));return 0;
}
#include <stdio.h>int main(void)
{char *lines[5] = //字符指针数组{"COSC1283/1984","Programming","Techniques","is","great fun"};char *str1 = lines[1]; // Programming char *str2 = *(lines + 3); //ischar c1 = *(*(lines + 4) + 6); //great fun的地址加6个字节再取值,fchar c2 = (*lines + 5)[5]; //指向第一条数据C,向后偏移5个,得到2的地址,再加入5,得到数据9char c3 = *lines[0] + 2;//得到第一条数据的第一个字符的ascii码值,C+2=Eprintf("str1 = %s\n", str1); printf("str2 = %s\n", str2); printf("c1 = %c\n", c1); printf("c2 = %c\n", c2); printf("c3 = %c\n", c3); return 0;
}

#include <stdio.h>int main(void)
{int i;int num;int (*p)[5] = NULL; //数组指针 int arr[5] = {5,2,4,5,7};p = &arr;printf("=====================================\n");num = sizeof(arr)/sizeof(arr[0]);for(i=0;i<num;i++){printf("*p[%d] = %d\n",i,*p[i]);}printf("=====================================\n");for(i=0;i<num;i++){printf("*(p+%d) = %p\n",i,*(p+i)); } printf("=====================================\n");for(i=0;i<num;i++){printf("p[%d] = %p\n",i,p[i]); }printf("=====================================\n");for(i=0;i<num;i++){printf("(*p)[%d] = %d\n",i,(*p)[i]); }return 0;
}

