你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

数组和指针有什么区别?

[复制链接]
gaosmile 发布时间:2020-8-11 20:18
一、能力错觉

当书本(或谷歌)摆在眼前时,大脑会产生错觉,以为学习材料也同样存入了大脑,阅读毕竟比回想简单多了。

以为反复的阅读资料就是自己已经掌握知识,这就是能力错觉。

解决能力错觉的方法:

  • 积极回想——让大脑提取关键概念,而非通过重复阅读被动地获取知识,这样才能高效地学习。& G) t: `- {& w2 ~% _+ [

现在网络上盛行各种it类的视频教程,我不否认不少视频教程是高质量的,但是所有视频类资料都有一个问题:

  • 可以让人不用阅读书籍,减少思考,只要被动地听老师们讲课就能舒舒服服地获取到知识,这很容易会让某些初级的软件开发人员形成能力错觉,仿佛视频里写过的代码,解决过的 bug 都是自己已经学到的知识似的。2 Z' c6 m: \+ [2 V

有效的解决办法是:

  • 公开学习笔记和练习代码。公开学习笔记的目的是借助外部压力,高效回想,进而提高自己的学习标准。

  • 另外,公开写作则会给你的写作增加很多维度的外部压力,你会想如何让别人更好地理解我要表达的意思;如何传递更多价值,让别人读完有所收获;如何让更多人看到;如何让别人读得下去;如何排版让大家看得更舒服;


    : c9 Z# d9 l6 ]+ p' s

二、数组和指针有什么区别?

正文目录:

1. 用于声明时两者有重大区别
# r. R. V" w3 ]2. 你真的理解声明和定义吗?
6 D8 Z) R+ [" B7 ~  r9 L) j5 Z3. 数组和指针的底层是如何访问数据的?0 I! w/ ?+ s( N$ D' ^1 J3 S& R, V
4. 哪些场景可以用指针代替数组?( j5 t- ^9 }& T& N/ {
5. 为什么C语言要把数组形参退化为指针?
7 S# e- }0 y$ N* ]2 a: T" e# i6. 如何使用指针访问多维数组?
( g' ]) X; |8 |! V7. 相关面试题
$ x% I9 G" s. ~# h) ]

写作目的:

  • 正确看待数组和指针。( O* v; v, r0 Q3 C5 s6 `3 q

测试环境:

  • Ubuntu 16.04
  • Gcc 5.4.0
      e2 a8 }- M' S( I8 }. F) a6 f4 s
微信图片_20200811201726.gif
1. 用于声明时两者有重大区别

1) 误导新手的说法:

由于数组和指针的所谓等价性非常接近,不少程序员有时忽视了二者之间的其他重要区别 ,最误导新手的说法之一就是 “数组和指针是相同的",这是一种非常危险的说法。

看下面这个例子:

extern int *x;
# F+ s  w# h$ z% X. c* v6 wextern int x[];

# x% i& I+ M; S( T
  • 第一条语句声明 x 是个 int 型的指针;

  • 第二条语句声明 x 是个 int 型数组,长度尚未确定,即存储长度在别处定义。

    6 |6 I6 ^/ f# c8 }/ S

2) 为什么有些人会误以为指针和数组总是可以互换?

最主要原因是:" ~. ~9 o8 `1 w! m4 b3 B
对数组的引用 ( x ) 总是可以写成对指针的引用 ( *(x+i) )。

  • 即确实存在一种指针和数组的定义完全相同的上下文环境。不幸的是,这只是数组的一种极为普通的用法,并非所有情况下都是如此。
    + I, U% @& m% b3 v0 G! q/ J) ?, T

2. 你真的理解声明和定义吗?

想要要真正理解为什么 extern int *x 不等于 extern int x[],我们首先需要搞清楚什么是声明,什么是定义。

1) 链接器的视角:

  • C 语言中的对象必须有且只有一个定义,但它可以有多个 extern 声明。这里所说的对象跟 C++ 中的对象并无关系,这里说的对象是 从链接器的视角来看的,链接器将各个函数、变量都视为对象。" q( _. H# J4 Y- L

2) 定义和声明的联系与区别:

  • 定义是一种特殊的声明,它创建了一个对象;声明简单地说明了在其他地方创建的对象的名字。

  • 定义只能出现在一个地方,它指定了对象的类型并分配内存以创建新的对象。声明可以多次出现 以描述对象的类型,用于指代其他地方定义的对象,它不为对象分配内存。

  • extern 对象声明告诉编译器对象的类型和名字,对象的内存分配则在别处进行。由于并未在声明中为数组分配内存,所以并不需要提供关于数组长度的信息 (多维数组例外)。


    8 F; L* `2 `9 r+ u( `3 |( @3 e

3) 总结成一句话:

  • 定义 = 声明 + 分配内存 (创建对象)
    * q/ i1 X1 A6 E5 D# {

4) 回过头来看这个例子:

extern int *x;8 F+ Q4 n* `. F8 o& Z
extern int x[];
  z3 N2 @2 O1 H1 {% `) P: S( G

前者声明了一个指针,后者声明了一个数组,那么它们对应的指针和数组的定义(最重要的是内存分配) 能相等吗?


3. 数组和指针的底层是如何访问数据的?

现在我们来看看指针和数组的定义与使用。

1) "地址 X (Address)" 和 "地址 X 的内容(Contents of Address)" 之间的区别:

对于"地址 X" 和 "地址 X 的内容",在 C 语言中是用同一个符号来表示这两样东西,由编译器根据上下文环境判断它的具体含义。

2) 看下面这个例子:

X = Y- |) y3 _0 G# l
  • 符号 X 的含义是 X 所代表的地址,它是左值,编译时可知;

  • 符号 Y 的含义是 Y 所代表的地址上的内容,它是右值,运行时才知;

  • 左值包括可修改的左值和不可修改的左值,C 语言中,一般的数据类型都是都可作为可修改的左值,只有数组是不可修改的左值;

  • 数组的地址在编译时可知,编译器有了这个地址 (即数组首地址),就可以直接进行读写操作。而指针必须在运行时取得它的当前值,然后才能对它进行解除引用操作,才能进行读写操作。


    6 p& S# z4 _2 m1 |  `* [

3) 数组和指针的访问方式是不同的:

char a[9] = "abcedefgh";
7 {; P! W, M, _  z1 f6 n1 v
微信图片_20200811201730.png
访问数组* U+ p& m6 z/ |) O; f( A! y
  • 上面这个例子中,a 是一个数组。

  • 在编译器符号表里有一个符号 a ,它的地址为9980;

  • 数组内的字符都可以从这个地址 + 偏移量找到,编译器甚至并不需要知道数组的总长度;

    # F, Q9 O. F9 g- Y
char c = 'F';8 U  N' L7 a- D8 K1 X: H# }! x
char *p = &c;
, E% B5 E( W$ i! U* l
微信图片_20200811201732.png
使用指针访问字符
8 v" b0 Z8 e* ]; H" [0 Q
  • 上面这个例子中,p 是一个指针。

  • 在编译器符号表中有一个符号 p, 它的地址为 4624;

  • p 指向的对象是一个字符。为了取得这个字符,必须得到地址 p 的内容 (5081),把它作为字符的地址并从这个地址中取得这个字符 ('F')。


    # u8 B* \* ~& v$ w7 I# R0 R' p

4) 当定义为指针 (char *p),并以数组方式 (p) 引用时会发生什么?

char *p = ”abcdefgh”
/ ~4 z8 Y4 ?" I! sprintf("%c\n", p[3]);
/ \* r# P" `+ F
: J. a/ N" _5 F* T* U5 u- ichar *a = ”abcdefgh”
- H6 l2 Y$ u/ V$ qprintf("%c\n", a[3]);

, |: p, n3 l1 \, W& l, T5 ~0 A9 K$ l
  • p[3] 和 a[3] 都能成功访问到字符 'd';

  • a 表示 "从 a 的地址开始,前进 i 步,每步都是一个字符(数组类型的长度)”;

  • p 表示 "从 p 所指的地址开始,前进 i 步,每步都是一个字符(即指针所指类型的长度)”;

  • 所以,当你用 extern char *p 来声明 char p[10]时,编译器会把 p 当成一个指针(Address),然后去获取 *(p) (即 Content of Addrss),这时最好的结果是程序立马崩溃,你能快点发现问题。最糟糕的情况是,程序崩溃在将来的某个时刻,你则 debug 到怀疑人生。

    ( ?3 G3 [1 U# H. k* }. N

4. 哪些场景可以用指针代替数组?

数组和指针容易混淆使用的 2 大类场景:

  • 声明

  • 在表达式中使用;

      |" [7 n; }" F. t) Z6 I

1) 声明:
$ `: t  x' Q% a1 Q# \声明的场景包括 3 种:

  • 1> 不可以的场景:定义也是一种声明,定义数组时不能用指针的形式;

  • 2> 不可以的场景:extern 数组时不能改写成指针的形式, 例如:

    3 q, _5 h( ?/ G' W/ g# F2 b5 k& C
int char[10];    // define
7 [) v, B2 C  V- N. U" rextern char a[]; // ok
! V, R! V; p$ p9 xextern char *a;  // error
. A5 H. C. W, [6 y* |6 m
  • 3> 可以的场景:函数的形参,用数组形式还是指针形式,随你自己的喜好。5 V8 l/ T! k3 \( x8 I& l  `

2) 在表达式中使用:

  • 在表达式中,指针形式和数组形式等效。2 E- @! B# v2 j

3) 几条重要的规则:

  • 规则1:"表达式中的数组名" 就是指针;

  • 规则2:把数组下标可当作指针的偏移量;

  • 规则3: "作为函数参数的数组名" 等同于指针;


    ' ^0 S  X3 O: D! P( S7 o/ a

5. 为什么 C 语言要把数组形参退化为指针?1) 出于效率的考虑:
  • 在 C 语言中,所有非数组形式的数据实参均以传值形式(对实参作一份拷贝并传递给调用的函数,函数不能修改作为实参的实际变量的值)。

  • 如果要拷贝整个数组,无论在时间上还是在内存空间上的开销都可能是非常大的。

    + @( U( g) S* n6 O2 U9 \

2) 出于简化编译器的考虑:

  • 在 C 语言中,所有的数组在作为参数传递时都转换为指向数组起始地址的指针,而其他的参数均采用传值调用。

  • 允许程序员把形参声明为数组 (程序员打算传递给函数的东西) 或者指针 (函数实际所接收到的东西)。在函数内部,编译器始终把它当作一个指向数组第一个元素的指针。


    ) H. _  L2 E3 r% y$ X

3) 看下面这个例子:

static int array[10], array2[10];# k6 ^* G8 \/ ]/ r* ~3 U
' R& i) M  Q: s. F1 e4 E- G
static void func1(int *ptr): F3 `% N7 w2 z' @1 l' o
{! r, K) d) B$ ?0 h' y( q( u% ?- K9 h
    ptr[1] = 3;
+ K# P( }. h, Q    *ptr = 3;
/ X5 Q5 n( j& _% [- V5 V& C    ptr = array2;0 Z2 l1 A( q3 B* v! V1 s6 }9 r
}
7 z$ y, c1 }3 ?7 v
% P3 b$ R4 Z4 p! v) V& xstatic void func2(int array[])+ C% @9 b, o5 u: {+ J) I
{. ?, v! c% E$ m1 J+ _) |, L4 Z6 B
    array[1] = 3;
- k# q( n( O8 ?$ B  a) t% I    *array = 3;
+ _# U) Q" a1 E' G2 i    array = array2;   // OK, because array is a pointer  x# f- J' ~4 X6 V+ V7 |9 N
    printf("*array=%d\n", *array);
; p* k% C" F3 c0 X}
* \9 a- V1 d7 M/ y
# K4 y( t. ~7 @3 Y$ N( T- iint main(void)" E) G0 Y' r: m" ]! e! g
{
% {5 e4 x8 K  o  o- |2 |% y    func1(array);! r0 d1 j. G; t
    func2(array);& G/ R5 K7 T7 A
    - F) U% e. h3 ]$ m, ]
    array[1] = 3;
! t. F" l# `  O/ F' L8 d5 g% S    *array = 3;
, V( x" A1 M% q3 j! G- R0 y    array = array2; // ERROR
& ~2 j7 F, N& k) [3 c. @    return 0;
; @2 m* Q4 k. _& Z+ e( M; X}
" N4 ^1 p+ K# ~9 B2 z

编译运行:

// main 中调用 array = array2时:
8 Y* G9 G2 v2 r- `# Q  h) R11: error: assignment to expression with array type
2 f- q7 E0 Q; H4 n- U
7 I! K* O5 z  t. `// 去掉 main / array = array2时:
6 x' ~) k7 n( e7 P8 n' V  X$ ./point_array_arg( E; N$ z4 T9 W8 v2 h
*array=0
) l2 @2 f" w  Y& j

6. 如何使用指针访问多维数组?

1) C 语言的多维数组:

  • 采用最右的下标先变化原则,其最大的用途是存储多个字符串;

  • 单个元素的存储和引用实际上是以线性形式排列在内存中;

  • 不能把一个数组赋值给另一个数组,因为数组作为一个整体不能成为赋值的对象;

  • 可以把数组名赋值给一个指针,是因为在表达式中的数组名被编译器当作一个指针;

  • 指针下标引用的规则告诉我们 pea[j] 被编译器解释为 ((pea + i) + j);

  • 可以通过声明一个一维指针数组 ( (char *)pea[4],下标方括号的优先级比指针的星号高),其中每个指针指向一个字符串,来取得类似二维字符数组的效果;


    % N" a& j  o/ b
微信图片_20200811201735.png
指针数组
3 W8 h. h2 K9 {" q/ b2 R0 ^7 Z/ d7. 相关面试题

1) 找错:计算字符串长度

下面这段程序是为了把字符串转换为大写:

#include <stdio.h>1 w( t3 x: r; {
8 S' X. @! e1 {" e0 {6 _. I
void UpperCase(char str[])
! n) ?3 V3 A. w& _. W/ K  j2 p' |$ }{2 u9 S# G/ W* S) I% N
    int test = sizeof(str);
- U  z; z( w' L; C5 Q6 U    int test2 = sizeof(str[0]);
5 l6 P) _& `! e; _4 }
  S/ r6 L( R' B: H/ ^  b    for(size_t i=0; i<sizeof(str)/sizeof(str[0]); ++i) {! W) B4 ^& n7 O! Q1 ]  j; z
        if('a'<=str && str<='z')0 l1 m# \% E9 E; m( `
            str -= ('a'-'A');& Q; Q! g* n4 T( Z2 b" j/ o
    }" o! B. d- r% L8 X% {
}
& ?( R5 ~( b# P& h2 \
5 T4 D$ Q. P8 ]0 g- \: J; `2 K9 q, i: @8 I

: W3 `6 j% j4 F' J( Y- s1 X& aint main(void)2 }. C9 J; d- ]3 V$ {
{
% \$ f+ S! o5 a- d4 p* r
+ n  J  v9 o7 _+ ?& m    char str[] = "aBcDeefGHijKL";" ^; a  B, U; X; ]* j( e
    printf("The length of str is %d\n", sizeof(str)/sizeof(str[0]));
4 a. s2 X8 g) F! p5 c1 F4 Q7 K9 B1 p4 Q2 M# V
    UpperCase(str);
& V0 s6 F' [5 p  s7 F5 k+ c    printf("result: %s\n", str);& D; h' E3 l+ p. p+ `" K; S
    return 0;
8 N) D6 `0 k+ E/ t. w, W. t}

: l% h7 l* t. h/ X$ b# v2 P, {

运行结果:

$ ./sizeof_array + R& f1 ~, F/ [" A, V) P) i. P
The length of str is 14) b- q5 D, c# R/ ~$ t6 o/ |. |
result: ABCDEEFGHijKL

% m# G2 G7 I7 Q1 F( \

问题出在 UpperCase() 里的 sizeof(str),这里的 str 是一个指针而不是数组。

正确的写法有2种:

  • 给UpperCase 传递长度参数,这是最稳妥的方式。
  • 在UpperCase 内使用strlen 获取字符串长度,这种方法仅适用于以 ‘\0’ 结尾的字符数组;
    5 G0 H2 E/ K' E9 t

) w3 O1 T. r1 I. V
赞 收藏 评论0 发布时间:2020-8-11 20:18

举报

0个回答
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版