指针与数组
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//指针与数组
- 指针与地址
- 指针与函数参数
- 指针与数组
- 地址算术运算
- 字符指针与函数
- 指针数组以及指向指针的指针
- 多维数组
- 指针数组的初始化
- 指针与多为数组
- 命令行参数
- 指向函数的指针
- 复杂声明
//指针与地址
一元运算符&可用于取一个对象的地址。如:
p=&c;
将把c的地址赋值给变量p,则称p为指向c的指针。地址运算符&只能应用于内存中的对象,即变量与数组元素。它不能作用于表达式,常量或register类型的变量。
一元运算符*是间接寻址或间接引用运算符。当它作用于指针时,将访问指针所指向的对象。假定x与y是整数,而ip是指向int类型的指针,下面的代码说明了如何在程序中声明指针以及如何使用运算符&和*:
int x=1 , y=2 ,z[10];
int *ip; // ip is a pointer to int
ip=&x; // ip now points to x
y=*ip; // y is now 1
*ip=0; // x is now 0
ip=&z[0]; // ip now points to z[0]
注意:指针只能指向某种特定类型的对象,即每个指针都必须指向某种特定的数据类型。
一个例外情况是指向void类型的指针可以存放指向任何类型的指针,但它不能间接引用其自身。
//指针与函数参数
由于C语言是以传值的方式将参数值传递给被调用函数。因此,被调用函数不能直接修改主调函数中变量的值。
指针参数使得被调用函数能够访问和修改主调函数中对象的值。
//指针与数组
在C语言中,通过数组下标所能完成的任何操作都可以通过指针来实现。一般来讲,用指针编写的程序比用数组下标编写的程序执行速度快,但另一方面,用指针实现的程序理解比较困难。
声明
int a[10];
定义了一个长度为10的数组a。即定义了一个由10个对象组成的集合,这10个对象存储在相邻的内存区域中,名字分别为a[0],a[1],a[2],...,a[9]。
a:|____|____|____|____|____|____|____|____|____|____|
a[0] a[1] a[2] a[9]
a[i]表示该数组的第i个元素。如果pa的声明为
int *pa;
则说明它是一个指向整型对象的指针,那么,赋值语句
pa=&a[0];
则可以将指针pa指向数组a的第0个元素,也就是说,pa的值为数组元素a[0]的地址
那么,赋值语句
x=*pa;
将把数组元素a[0]中的内容复制到变量x中。
如果pa指向数组中的某个特定元素,那么,根据指针运算的定义,pa+1将指向下一个元素,pa+i将指向pa所指向元素之后的第i个元素,而pa-i将指向pa所指向数组元素之前的第i个元素。因此,如果指针pa指向a[0],那么*(pa+1)引用的是数组元素a[1]的内容,pa+i是数组元素a[i]的地址,*(pa+i)引用的是数组元素a[i]的内容。
下标和指针运算之间有密切的对应关系。根据定义,数组类型的变量或表达式的值是该数组第0个元素的地址。执行赋值语句:
pa=&a[0];
后,pa和a具有相同的值。因为数组名所代表的就是该数组最开始的一个元素的地址,所以,赋值语句pa=&a[0]也可以简写为:
pa=a;
对于数组元素a[i]的引用也可以写成*(a+i)的形式。在计算数组元素a[i]的值时,C语言实际上先将其转换为*(a+1)的形式,然后再进行求值,因此在程序中这两种形式是等价的。如果对这两种等价的表示形式分别施加地址运算符&,便可得出这样的结论:&a[i]和a+i的含义也是相同的。a+i是a之后第i个元素的地址。相应的,如果pa是个指针,那么,在表达式中也可以在它的后面加下标。pa[i]与*(pa+i)是等价的。简言之,一个通过数组和下标实现的表达式可等价地通过指针和偏移量实现。
但是,数组名和指针之间有一个不同之处,指针是一个变量,因此,在C语言中,语句pa=a和pa++都是合法的。但数组名不是变量,因此,类似于a=pa和a++形式的语句是非法的。
当数组名传递给一个函数时,实际上传递的是该数组第一个元素的地址。在被调用函数中,该参数是一个局部变量,因此,数组名参数必须是一个指针,也就是一个存储地址值的变量。可以利用该特性辨析strlen函数的另一个版本,该函数用于计算一个字符串的长度。
// strlen: return length of string s
int strlen(char *s)
{
int n;
for(n=0; *s!='\0';s++)
n++;
return n;
}
因为s是一个指针,所以对其执行自增运算是合法的。执行s++运算不会影响到strlen函数的调用者中的字符串,它仅对该指针在strlen函数中的私有副本进行自增运算。因此,类似于下面这样的函数调用:
strlen("hello world"); // string constant
strlen(array); // char array[100]
strlen(ptr); // char *ptr
都是正确的。
在函数定义中,形式参数
char s[];
和
char *s;
是等价的。通常习惯使用后一种形式,因为它比前者更直观地表明了该参数是一个指针。如果将数组名传递给函数,函数可以根据情况判定是按照数组处理还是按照指针处理,随后根据相应的方式操作该参数。为了直观且恰当地描述函数,在函数中甚至可以同时使用数组和指针这两种表示方法。
也可以将指向子数组起始位置的指针传递给函数,这样,就将数组的一部分传递给了函数。例如,如果a是一个数组,那么下面两个函数调用:
f(&a[2]);
与
f(a+2);
都将把起始于a[2]的子数组的地址传递给函数f。在函数f中,参数的声明形式可以为
f(int arr[]){ ... }
或
f(int *arr){ ... }
对于函数f来讲,它并不关心所引用的是否只是一个更大数组的部分元素。
如果确信相应的元素存在,也可以通过下标访问数组第一个元素之前的元素。类似于p[-1],p[-2]这样的表达式在语法上都是合法的,它们分别引用位于p[0]之前的两个元素。当然,引用数组边界之外的对象是非法的。
//地址算术运算
如果p是一个指向数组中某个元素的指针,那么p++将对p进行自增运算并指向下一个元素,而p+=i将对p进行加i的增量运算,使其指向指针p当前所指向的元素之后的第i个元素。这类运算是指针或地址算术运算中最简单的形式。
一般情况下,同其他类型的变量一样,指针也可以初始化。通常,对指针有意义的初始化值只能是0或者是表示地址的表达式,对后者来说,表达式所代表的地址必须是在此前已定义的具有适当类型的数据的地址。例如,声明
static char* allocp=allocbuf;
将allocp定义为字符类型指针,并将它初始化为allocbuf的起始地址,该起始地址是程序执行时的下一个空闲位置。上述语句也可以写成下列形式:
static char* allocp=&allocbuf[0];
这是因为该数组名实际上就是数组第0个元素的地址。
指针与整数之间不能相互转换,但0是唯一的例外,常量0可以赋值给指针,指针也可以和常量0进行比较。程序中经常使用符合常量NULL代替常量0,这样便于更清晰的说明常量0是指针的一个特殊值。符号常量NULL定义在标准头文件<stddef.h>中。
指针算术运算有以下几个重要特点:
首先,在某些情况下对指针可以进行比较运算。例如,如果指针p和q指向同一个数组的成员,那么它们之间就可以进行类似于==,!=,<,>=的关系比较运算。如果p指向的数组元素的位置q指向的数组元素位置之前,那么关系表达式
p<q
的值为真。任何指针与0进行相等或不等的比较运算都有意义。但是,指向不同数组的元素的指针之间的算术或比较运算没有意义。(特例:指针的算术运算中可使用数组最后一个元素的下一个元素的地址)
其次,指针可以和整数进行相加或相减运算。例如,结构
p+n
表示指针p当前指向的对象之后第n个对象的地址。无论指针p指向的对象是何种类型,上述结论都成立。在计算p+n时,n将根据p指向的对象的长度按比例缩放,而p指向的对象的长度则取决于p的声明。例如,如果int类型占4字节的存储空间,那么在int类型的计算中,对应的n将按4的倍数来计算。
指针的减法运算也是有意义的,如果p和q指向相同的数组元素,且p<q,那么q-p+1就是位于p和q指向的元素之间的元素的数目。由此可以编写出函数strlen的另一个版本:
// strlen: return length of string s
int strlen(char *s)
{
char *p=s;
while(*p!='\0')
p++;
return p-s;
}
指针p被初始化为指向s,即指向该字符串的第一个字符。while循环语句将一次检查字符串中的每个字符,直到遇到标识字符数组结尾的字符‘\0’为止。由于p是指向字符的指针,所以没执行一次p++,p就将指向下一个字符的地址,p-s则表示已经检查过的字符数,即字符串长度。
注意:字符串中的字符数有可能超过int类型所能表示的最大范围。头文件<stddef.h>中定义的类型ptrdiff_t足以表示两个指针之间的带符号差值。但是,在这里使用size_t作为函数strlen的返回值类型,这样可以与标准库中的函数版本相匹配。size_t是由运算符sizeof返回的无符号整型。
有效的指针运算包括相同类型指针之间的赋值运算;指针同整数之间的加法或减法运算;指向相同数组中元素的两个指针间的减法或比较运算;将指针赋值为0或指针与0之间的比较运算。其他所有形式的指针运算都是非法的,例如两个指针间的加法、乘法、除法、移位或屏蔽运算;指针同float或double类型之间的加法运算;不经强制类型转换而直接将指向一种类型对象的指针赋值给指向另一种类型对象的指针的运算(两个指针之一是void*类型的情况除外)。
//字符指针与函数
字符串常量是一个字符数组,例如:
“I am a string"
在字符串的内部表示中,字符数组以空字符‘\0’结尾,所以,程序可以通过检查空字符找到字符数组的结尾。字符串常量占据的存储单元数也因此比双引号内的字符数大1。
字符串常量最常见的用法也许是作为函数参数,如:
printf("hello, world\n");
当类似于这样的一个字符串出现在程序中时,实际上是通过字符指针访问该字符串的。在上述语句中,printf接受的是一个指向字符数组第一个字符的指针。也就是说,字符串常量可通过一个指向其第一个元素的指针访问。
除了作为函数参数外,字符串常量还有其他用法。假定指针pmessage的声明如下:
char *pmessage;
那么,语句
pmessage="now is the time";
将把一个指向该字符数组的指针赋值给pmessage。该过程并没有进行字符串的复制,而只是涉及到指针的操作。C语言没有提供将整个字符串作为一个整体进行处理的运算符。
下面两种定义之间有很大的差别:
char amessage[]="now is the time"; //定义一个数组
char *pmessage="now is the time"; //定义一个指针
其中,amessage是一个仅仅足以存放初始化字符串以及空字符'\0'的一维数组。数组中的单个字符可以进行修改,但是amessage始终指向同一个存储位置。另一方面,pmessage是一个指针,其初始值指向一个字符串常量,之后它可以被修改以指向其他地址,但如果试图修改字符串的内容,结果是不可确定的。
为了更近一部讨论指针和数组其他方面的问题,下面以标准库中两个有用的函数为例来说明。
第一个函数strcpy(s,t),把指针t指向的字符串复制到指针s指向的位置。如果使用语句s=t实现该功能,其实质上只是拷贝了指针,而并没有赋值字符。为了进行字符的复制,这里使用了一个循环语句。strcpy函数的第一个版本是通过数组方法实现的,如下:
// strcpy :copy t to s; array subscript version
void strcpy(char *s, char *t)
{
int i;
i=0;
while(( s[i] = t[i] ) !='\0')
i++;
}
下面使用指针方法实现strpy函数
//strcpy : copy t to s; pointer version
void strcpy(char *s, char *t)
{
int i;
i=0;
while( ( *s = *t ) != '\0' )
{
s++;
t++;
}
}
因为参数是通过值传递的,所以在strcpy函数中可以以任何方式使用参数s和t。在此,s和t是方便地进行了初始化的指针,循环每执行一次,他们就沿着相应的数组前进一个字符,直到将t中的结束符'\0'复制到s为止。
实际上,strcpy函数并不会按照上面的这些方式编写。经验丰富的程序员更喜欢对它编写成下列形式:
// strcpy : copy t to s; pointer version 2
void strcpy(char *s, char *t)
{
while( (*s++ = *t++) != ‘\0’ )
;
}
在该版本中,s和t的自增运算放到了循环的测试部分中。表达式*t++的值是执行自增运算之前t所指向的字符。后缀运算符++表示在读取该字符之后才改变t的值。同理,在s执行自增运算前,字符就被存储到指针s指向的旧位置。该字符值同时也用来和空字符'\0'进行比较运算,以控制循环的执行。最后的结果是依次将t指向的字符复制到s指向的位置,知道遇到结束符位置(同时也复制该结束符)。
为了更进一步的精炼程序,我们注意到,表达式同'\0'的比较是多余的,因为只需要判定表达式的值是否为0即可。因此,该函数可进一步写成下列形式:
// strcpy : copy t to s; pointer version 3
void strcpy(char *s , char *t)
{
while(*s++=*t++)
;
}
该函数初看起来不太容易理解,但这种表示方法很有好处,应该掌握此种方法,C语言程序中经常会采用这种写法。
标准库<string.h>中提供的函数strcpy把目标字符串作为函数值返回。
第二个函数是strcmp(s,t)。该函数比较字符串s和t,并且根据s按照字典顺序小于、等于或大于t的结果分别返回负整数、0或正整数。该返回值是s和t由前向后逐字符比较时遇到的第一个不相等字符处的字符的差值。
// strcmp : return <0 is s<t, 0 if s==t, >0 if s>t
int strcmp(char *s, char *t)
{
int i;
for(i=0; s[i]==t[i];i++)
if(s[i]=='\0')
return 0;
return s[i]-t[i];
}
下面是用指针方式实现
// strcmp return <0 if s<t, 0 if s==t, >0 if s>t
int strcmp(char *s, char *t)
{
for(; *s==*t; s++, t++)
if(*s=='\0')
return 0;
return *s-*t;
}
由于++和--既可以作为前缀,也可以作为后缀运算符,所以还可以将运算符*与运算符++和--按照其他方式组合使用,但这些用法不多见。例如,
*--p;
在读取制作p指向的字符之前先对p指向自减运算。事实上,下面的两个表达式:
*p++=val; //将val压入栈
val=*--p; //将栈顶元素弹出到val中
是进栈和出栈的标准用法。
//指针数组以及指向指针的指针
指针数组,其定义方式如下:
char lineptr[MAXLINES]; // getline函数
它表示lineptr是一个具有MAXLINES个元素的一维数组,其中数组的每个元素是一个指向字符类型对象的指针。也就是说,lineptr[i]是一个字符指针,而*lineptr[i]是该指针指向的第i个文本行的首字符。
//多维数组
在C语言里,二位数组实际是一种特殊的一维数组,它的每个元素也是一个一维数组。
数组可以用花括号括起来的初值表进行初始化,二维数组的每一行由相应的子列表进行初始化。
注意:
int daytab[3][12];
如果将二维数组作为参数传递给函数,那么在函数的参数声明中必须指名数组的列数。数组的行数没有太大关系,函数调用时传递的是一个指针,它指向由行向量构成的一维数组,其中每个行向量是具有13个整型元素的一维数组。在该例子中,传递给函数的是一个指向很多对象的指针,其中每个对象是由13个整型元素构成的一维数组。因此,如果将数组daytab作为参数传递给函数f,那么f的格式应写作:
f( int daytab[3][13] ){...}
或
f( int daytab[][13] ){...}
因为数组的函数无关紧要,所以,该声明还可写成
f( int (*daytab)[13])
这种声明形式表明参数是一个指针,它指向具有13个整型元素的一维数组。因此方括号[]的优先级高于*的优先级,所以上述声明中必须使用圆括号。如果去掉括号,则声明变成
int *daytab[13]
这相当于声明了一个数组,概述组有13个元素,其中每个元素都是一个指向整型对象的指针。一般来说,除数组的第一维下标可以不指定大小外,其余各维都必须明确指定大小。
//指针数组的初始化
//指针与多维数组
二位数组与指针数组之间的区别。假如有下面两个定义:
int a[10][21];
int *b[10];
那么,从语法角度讲,a[3][4]和b[3][4]都是对一个int对象的合法引用。但a是一个真正的二维数组,它分配了200个int类型长度的存储空间,并且通过常规的矩阵下标计算公式20*row+col(row行,col列)计算得到a[row][col]的位置。但是,对b来说,该定义仅仅分配了10个指针,并且没有对它们初始化,它们的初始化必须以显式的方式进行,比如静态初始化或通过代码初始化。假定b的每个元素都指向一个具有20个元素的数组,那么编译器就要为它分配200个int类型长度的存储空间以及10个指针的存储空间。指针数组的一个重要优点在于,数组的每一行长度可以不同,也就是说,b的每个元素不必都指向一个具有20个元素的向量,某些元素可以指向具有2个元素的向量,某些元素可以指向有50个元素的向量,而某些元素可以不指向任何向量。
指针数组最频繁的用处是存放具有不同长度的字符串,如下定义:
char *name[]={"Illegal month","Jan","Feb","Mar"};
下面是二维数组的声明:
char aname[][15]={"Illegal month", "Jan", "Feb", "Mar"};
下面是指针数组和二维数组的图形化描述:
//命令行参数
在支持C语言的环境中,可以在程序开始执行时将命令行参数传递给程序。调用主函数main时,它带有两个参数。第一个参数(习惯上成为argc,用与参数计数)的值表示运行程序时命令行中参数的数目;第二个参数(称为argv,用于参数向量)是一个指向字符串数组的指针,其中每个字符串对应一个参数。通常使用多级指针处理这些字符串。
按照C语言的约定,argv[0]的值是启动该程序的程序名,因此argc的值至少为1。另外,ANSI标准要求argv[argc]的值必须为一空指针。
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//在C语言中,指针和数组名通常都可以混用。
如:
char *p; // *p=p[0],*(p+1)=p[1]
char b[5]; // b[0]=*b,b[2]=*(b+2)
在一般的通信中(如串口),通常都使用字节传输。而像float,long,int之类的,有4个字节。我的方法就是取它的地址,强制转换为char型指针,然后当作数组来用。
float x;
SBUF=((char*)&x)[0];
SBUF=((char*)&x)[1];
SBUF=((char*)&x)[2];
SBUF=((char*)&x)[3];
接收时,刚好倒过来。更有趣的是,对于数组形式,数组名和后面的偏移量可以随便换。
char buff[10]; //或者用char *buff=&buffer;
buff[3]=0xaa;
3[buff]=0xaa; //两者是一样的
因此,可以认为编译器是这么认为的,对于形如xxx[yyy]这样的表达式,会转化为*(xxx+yyy),因此两种形式都是一样的。
//数组名和指针的区别
- 数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组(如示例1中: sizeof(str)=10)
- 数组名的外延在于其可以转换为指向其指代实体的指针,而且是一个指针常量(如示例2中: strcpy(str2,str1))
- 指向数组的指针则是另外一种变量类型,仅仅意味着数组的存放地址
注意:虽然数组名可以转换为指向其指代实体的指针,但是它只能被看作一个指针常量,不能被修改,如下:
int intArray[10];
intArray++; //错误
1.数组名不是指针

#include <stdio.h>
int main(int argc, char* argv[])
{
char str[10];
char *pStr;
pStr=str;
printf("str len=%d\n",sizeof(str));
printf("pStr len=%d\n", sizeof(pStr));
return 0;
}
示例程序1执行结果:
str len=10
pStr len=4
执行示例程序可知:sizeof(str)=10,sizeof(pStr)=4,可见数组名和指针是不一样的
2.数组名神似指针

#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
char str1[10]="I miss u";
char str2[10];
strcpy(str2,str1);
printf("str is:%s\n",str1);
printf("pStr is:%s\n",str2);
return 0;
}
示例程序2执行结果:
str is:I miss u
pStr is:I miss u
标准C库函数strcpy的函数原型中能接纳的两个参数都为char型指针,而示例程序中传给它的确实两个数组名。
3.数组名可能失去其数据结构内涵

#include <stdio.h>
#include <string.h>
void arrayTest(char str[])
{
printf("str len=%d\n",sizeof(str));
}
int main(int argc, char* argv[])
{
char str1[10]="I miss u";
arrayTest(str1);
return 0;
}
示例程序3执行结果:str len=4
分析:
数组名作为函数形参时,在函数体内,其失去了本身的内涵,仅仅只是一个指针;
在失去其内涵的同时,它还失去了其常量特性,可以作自增,自减等操作,可以被修改;
结果:
所以数组名作为函数形参时,就是一个普通的指针,4字节
//(*(a+1))和(&a+1)的区别

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
int a[5]={1,2,3,4,5};
int *ptr=(int *)(&a+1);
printf("*(a+1)=%d,*(ptr-1)=%d\n",*(a+1),*(ptr-1));
return 0;
}
程序执行结果:
*(a+1)=2,*(ptr-1)=5
显然*(a+1)=a[1],而&a+1是什么呢?&a不是首地址加1,而是加上1个a数组大小的偏移(sizeof(a))。所以ptr=a+5,*(ptr-1)=a[4]。因为&a相当于一个数组指针int (*)[5],该指针加1相当于移动5个int的存储空间,因此ptr=a+5.(参考:http://www.cnblogs.com/dolphin0520/archive/2011/09/26/2191985.html)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理