指针&数组&字符串

  无情的搬砖机器= =

一、指针 & 数组(细品)

  指针就是指针,指针变量在32位系统下,永远占4字节,其值为某一个内存的地址。指针可以指向任何地方(内存),但是不是任何地方你都能通过这个指针变量访问到呢?(有地址,无[权限]空间)

  数组就是数组,其大小与元素的类型和个数有关;定义数组时,必须指定其元素的类型和个数;数组可以存任何类型的数据,但是不能存函数。

总结如下:

  • 指针本质上也是一个变量
  • 指针要占用一定的内存空间(任何类型的指针的内存大小是一样的)
  • 指针用于保存内存地址的值

数组&结构体

  • 数组:所有的C/C++编译器在排列数组的单元时,总是把各个数组单元存放在连续的存储区里,单元和单元之间没有空隙
  • 结构体:在存放结构对象的各个成员时,在某种编译环境下,可能会需要字对齐或双字对齐或者是别的什么对齐,需要在相邻两个成员之间加若干个”填充字节”,这就导致各个成员之间可能会有若干个字节的空隙。

二、数组

2.1 数组的基础

  1. 数组在一片连续的内存中存储元素
  2. 元素的个数可以是显示的也可以是隐式的
    1
    int a[5]={1,2};//其中,其他的元素都为0来填充

2.2 数组地址与数组名

  1. 数组名代表数组首元素的地址
  2. 数组的地址需要取地址才可以得到
  3. 数组首元素的地址值 与 数组的地址值 相同(注意是值)
  4. 数组首元素的地址 &a[0] 与数组的地址 &a 是两个不同的概念

  举例,湖南省的省政府在长沙,长沙的市政府也在长沙,两个政府都在长沙。

2.3 数组名的盲点

  数组名如何理解呢?用来存放数组的区域是一块在栈中静态分配的内存(非static),而数组名是这块内存的代表,它被定义为这块内存的首地址。这就说明了数组名是一个地址,而且,还是一个不可修改的常量,完整地说,就是一个地址常量。

  数组名跟枚举常量类似,都属于符号常量。数组名这个符号,就代表了那块内存的首地址。注意了!不是数组名这个符号的值是那块内存的首地址,而是数组名这个符号本身就代表了首地址这个地址值,它就是这个地址,这就是数组名属于符号常量的意义所在。

  由于数组名是一种符号常量,因此它是一个右值,而指针,作为变量,却是一个左值,一个右值永远都不会是左值,那么,数组名永远都不会是指针!

  1. 数组名可以看作一个常量指针(指针所指向的内容不能改变)
  2. 既然是指向常量,那就不能作为表达式左值,因为左值能被修改
  3. 可以单独修改数组元素,但用数组名(实际上是符号常量)不能直接修改数组

2.4 指针 & 数组 的定义与声明(经典错误)

2.4.1 定义为指针,声明为数组

1
2
3
4
5
6
7
8
9
10
11
//test1 文件1
char * p="Hello World!";

//test2 文件2
#include<stdio.h>
extern char p[];

void main()
{
printf("%s\n",p);//printf("%s\n",*(unsigned int *)p);正确
}

2.4.2 定义为数组,声明为指针

1
2
3
4
5
//test1 文件1
char a[100];

//test2 文件2
extern char *a;

2.5 定义数组类型

  C语言中,可以通过typedef为数组类型重命名

1
typedef type(name)[size];

数组类型(重命名了一种一个数组类型):

1
2
typedef int(AINT5)[5];
typedef float(AFLOAT10)[10];

数组定义:

1
2
AINT5 i = {0,1,2,3,4};
AFLOAT10 f;

三、 字符串

  • 从概念上讲,C语言中没有字符串数据类型
  • 在C语言中使用字符数组来模拟字符串
    • 单引号' '括起来的一个字符代表整数
    • 双引号" "括起来的字符(串)代表一个指针
  • 字符串是以 '\0'结束的字符数组
  • 字符串可以分配于栈空间,堆空间或者 字符常量区(不能被改变)
1
2
3
char* s1 = "Hello World"//在字符常量区,不可改变这个字符串中的字符

char s2[100] = "Hello world"; //在栈上,可修改(不能通过数组名!)

3.1 (实)字符数组、(虚)字符串

  下面的代码合法吗?使用它有什么隐患?

1
2
3
4
char a[3] = "abc";
char *p = &a;
p[0]= 'I';
答案与分析:

  在标准C中这是合法的,但是它的生存环境非常狭小;它定义一个大小为3的字符数组,初始化为“abc”;注意,它没有通常的字符串终止符'\0',因此这个数组只是看起来像C语言中的字符串,实质上却不是。因此所有对字符串进行处理的函数,比如strcpy、printf等,都不能够被使用在这个假字符串上。

3.2 字符串的长度

  • 字符串的长度就是字符串说包含字符的个数
  • C语言中的字符串的长度值得是第一个'\0'字符串出现的字符个数
  • C语言中通过'\0'结束来确定字符串的长度

strlen()为例,strlen()为无符号类型

1
2
3
4
5
//比较两个字符串的长度的时候
//正确用法
if(strlen(a)>=strlen(b))
//错误用法
if(strlen(a)-strlen(b)>0)

3.3 不受限制的字符串函数

  不受限制的字符串函数是通过寻找字符串得结束符 '\0'来判断长度。

  • 字符串复制函数: char* strcpy(char* dst,const char* src)
  • 字符串连接: char* strcat(char* dst,const char* src)
  • 字符串比较函数:int strcmp(const char* s1,const char* s2)

注意事项

  • 不受限制的字符串函数都是以'\0'作为结束标记来进行的,因此输入参数必须包含'\0'
  • strcat和strcpy必须保证目标字符数组的剩余空间足以保存整个源字符串
  • strcmp以0值表示两个字符串相同
    • 第一个字符串大于第二个字符串的时候返回值大于0
    • 第一个字符串小于第二个字符串的时候返回值小于0
    • strcmp不会修改参数值,但依然以'\0'作为结束符号

3.4 长度受限制的字符串函数

  长度受限的字符串函数,是接收一个长度参数用于限定操作的字符串,提高程序的安全性。

  • 字符串复制:char* strncpy
  • 字符串连接:char* strncat
  • 字符串比较:char* strncmp

四、数组参数和指针参数(针对函数传入)

  数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:

  • 不允许拷贝数组
  • 使用数组时通常会将其转换成指针

因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。

因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。(数组参数退化为指针)

尽管不能以值传递的形式传递数组,但是我们可以把形参写成类似数组的形式:

1
2
3
4
5
6
7
8
//尽管形式不同,但这三个进行print函数是等价的
//每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]);
void print(const int[10])//这里的维度表示我们期望数组含有多少元素,实际不一定

//第三种容易被误会,且容易造成数组越界:
//实际元素不确定,但是函数操作(传入数组较短)以10个操作,越界

当编译器处理对print函数的调用时,只检查传入的参数是否是const int*类型:

1
2
3
4
int i = 2;
int j[2] = {1,2};
print(&i); //正确,&i的类型是int*
print(j); //正确,j被转换成int*并指向j[0]

如果我们传给printf函数的是一个数组,则实参自动地转换成指向首元素的指针,数组的大小对函数的调用没有影响。

五、二维数组

5.1 二维数组参数和指针参数

  C语言的编译器会让(不论是一维数组还是二维数组)数组参数退化为指针。C语言中无法向一个函数传递任意的多维数组。

  • 二维数组参数同样存在退化问题。
    • 二维数组可以看作是一维数组
    • 二维数组中的每一个元素是一维数组
  • 二维数组参数中第一维的参数可以省略(退化过程)

注意事项

  • C语言中无法向一个函数传递任意的多维数组
  • 为了提供正确的指针运算,必须提供除一维之外的所有维的长度(程序写法)
    限制
    • 一维数组-必须提供结束的标志
    • 二维数组-不能直接传递给函数
    • 多维-无法使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
#include<malloc.h>
void access(int a[][3],int row)
{
int col=sizeof(*a)/sizeof(int);//去推导出列的数量
int i=0,j=0;
for(i=0;i<row;i++)
{
for(j=0;j<col;j++)
{
printf("%d ",a[i][j]);
}
printf("\n");
}
}
void main()
{
int a[3][3]={
{1,2,3},
{4,5,6},
{7,8,9}
};
access(a,3);
}

5.2 二维数组与数组指针

  • 二维数组在内存中以一维的方式排布
  • 二维数组中的第一维是一维数组
  • 二维数组中的第二维才是具体的值
  • 二维数组的数组名同样可以看作常量指针
  • 二维数组同样代表数组首元素的地址
1
2
3
4
5
6
7
8
#include<stdio.h>
int main()
{
int a[5][5];
int (*p) [4];
p=a;
printf("%d\n",&p[4][2]-&a[4][2]);
}

答案为-4,因为(*p)一次跨越4个,而a一次跨越5个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//用指针遍历二维数组
#include<stdio.h>
int main()
{
int i,j;
int a[3][3]={{}};
for(i=0;i<3;i++)
for(j=0;j<3;j++)
*(*(a+i)+j)=1;
for(i=0;i<3;i++)
for(j=0;j<3;j++)
printf("%d",*(*(a+i)+j));

return 0;
}

六、指向指针的指针

为什么需要指向指针的指针?

  • 指针在本质上也是一个变量
  • 对于指针来讲也有传值调用与传址调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//重置动态空间
#include<stdio.h>
#include<malloc.h>
int rest(char** p,int size,int new_size)
{
int ret=1;
int len=0;
int i=0;
char* mid=NULL;
char* pt=NULL;
char* pp=*p;
if((p!=NULL)&&(new_size>0))
{
mid=(char*)malloc(3);
pt=mid;
len=(size<new_size)?size:new_size;
for(i=0;i<len;i++)
{
*pt++=*pp++;
}
free(*p);
*p=pt;
}else{
ret=0;
}
return ret;
}
void main()
{
char *p=(char*)malloc(5);
printf("%0X\n",p);
if(rest(&p,5,3))
{
printf("%0X\n",p);
}
}
-------------本文结束感谢您的阅读-------------