Programming

你想知道的 vfork(2) 的全部

最近vfork(2)的问题一再被问到,这里做一个总结,以求终止关于它的疑问。:-)

1. 为啥会有vfork(2)这个东西?

man page中说得很清楚:

However, in the bad old days a fork(2) would require making a complete copy
of the caller’s data space, often needlessly, since usually immediately afterwards an
exec(3) is done. Thus, for greater efficiency, BSD introduced the vfork() system call…
所以其实就是历史原因。从这里我们也可以看出,既然Linux的fork(2)实现使用了COW,所以现在再使用vfork(2)是没太大必要的。后面还提到:
This system call will be eliminated when
proper system sharing mechanisms are implemented. Users should not
depend on the memory sharing semantics of vfork() as it will, in that
case, be made synonymous to fork(2).
2. 为什么使用vfork(2)的限制有那么多?
the behavior is undefined if the process created by vfork()
either modifies any data other than a variable of type pid_t used to
store the return value from vfork(), or returns from the function in
which vfork() was called, or calls any other function before success-
fully calling _exit(2) or one of the exec(3) family of functions.
首先,为什么我只能在其后调用execve(2)和_exit(2)?看内核源代码,vfork(2)其实是通过completion实现的,在do_fork()时wait_for_completion(),而只有在do_execve()和do_exit()时才complete()(mm_release()),所以这就决定了你除了调用这两个系统调用外调用别的都是错的。

其次,为什么我不能从当前的函数中返回?很简单,因为vfork()的实现用的是CLONE_VM,它决定了父子进程之间是完全共享内存的(而不是COW)。所以如果子进程对堆栈内容做了修改,对父进程也会有影响。

一个随之而来的问题是:既然它们共享内存,那为什么子进程执行execve(2)就不会覆盖父进程呢?因为虽然CLONE_VM让do_fork()跳过了复制mm_struct,但是do_execve()在bprm_mm_init()时又创建了一个新的mm_struct。

3. 什么时候我应该使用vfork(2)而不是fork(2)?

90%以上的情况下你不应该这么做。假设父子进程哪个先运行很可能是一个不好的设计,你应该重新设计一下你的程序,或者考虑用其它方法实现。

随机选择午饭

您是否也像我们一样为吃午饭而烦恼?工作日午饭时间不多,周围的餐馆也不多,翻来复去吃的也就那么几样,而且都快吃腻了!怎么办?

这个脚本可以为您解除这个烦恼,每天为您随即选择午饭,并且会在午饭时间提醒您和您的饭友该去吃午饭!从此您再也不用为午饭该吃什么而苦恼!

注意:

1. 用 crontab -e 来定时,你知道该怎么做。:)
2. sendpatchset是我们发送补丁的一个脚本,你完全可以把它替换成别的,比如mutt。我之所以没用是因为我的那个台式机上没有配置mutt。恩,用git send-email应该也可以。
3. 你如果想用,你知道该修改哪里。

[bash]

!/bin/bash

RESTAURANTS=(‘Ji Tian Xiao Chi’ ‘Lao Jia Rou Bing. Huh?’ ‘-1 floor! Bad bad…’ ‘Xin Jiang Mian Guan’ ‘Kuai Le Jia Yuan’ ‘Tan Zi Rou’)

choice=$((${RANDOM}%${#RESTAURANTS[*]}))
everything=””
for ((i=0; i$choice_file <$control_file <<eof
SMTP: smtp.corp.redhat.com
From: Amerigo Wang
To: xxxxxx@redhat.com
Cc: xxxxxx@redhat.com
Subject: [LUNCH] $subject
File: $choice_file
EOF

sendpatchset $control_file
[/bash]

Intel SMM 缓存中毒攻击

一大早起来上网就看到这么一条劲爆的新闻。其实这个漏洞由来已久,以前至少见过两次对它的介绍,而今天,我们终于看到实实在在的exploit了。

我把官方的paper和exploit代码通读了一遍,发现这个漏洞的原理其实很简单,大体如此:

1. 把SMM内存区域设为write-back模式,这需要修改mtrr(/proc/mtrr)。很明显,在linux上这一步是需要root权限的。也就是说,这里也就已经决定了这个漏洞不是用来给你提升权限的,而是用来隐藏rootkit等东西的。

2. 对SMM内存区域写入你自己的攻击代码,因为上一步已经把cache设为了wb,所以写入的代码其实是暂时存放在cache中的。而在一般情况下,这块内存显然是uncachable的,而且不会让你随便写入的。

3. 马上进入SMM模式,这时,SMI中断处理程序会从cache中读取相应的代码去执行,而不是从内存中。攻击目的达成!而且系统根本不会检测得到!

这里面隐含了非常关键的一步,那就是需要知道SMM内存中SMI处理函数的偏移(详细见Intel官方手册Vol 3 24.4.1),官方paper中给出的方法是:把上面的手法反过去,去读出SMM里面的内容进而得到那个偏移,非常巧妙。:-) 这个漏洞好啊,SMM那个地方风不着雨不着的,而且权限还不低,更可恶的是攻击代码在缓存中,几乎不太可能检测得到!tmd绝了!

Update: 根据这篇文章,TSEG其实就是物理内存的最后一块,可以根据自己机器的配置和/proc/mtrr的输出来判断。

这里有几个概念可能不太好理解,

1. SMM,System Management Mode。Intel CPU的一种模式,和我们通常所说的实模式,保护模式类似。它和实模式差不多,只是寻址是32位的。设立它的目的是给BIOS和firmware使用的。详细描述见Intel Vol 3, Ch 24。

2. MSR,MODEL-SPECIFIC REGISTERS,一些特殊的寄存器,有特殊的用途,比如sysenter。这些寄存器可以通过rdmsr来读取,通过wrmsr来写入。详细描述见 Intel Vol 3 8.4。

3. MTRR,MEMORY TYPE RANGE REGISTERS,MSR中的一类,设置内存块的缓存属性,比如上面提到的缓存策略。具体描述见Intel Vol 3 10.11,也可参考Linux内核源代码arch/x86/kernel/cpu/mtrr/中的代码。

用bash自己计算CPU和内存使用率

如果你不自己计算,最简单的办法就是去解析ps的输出(top的应该比较麻烦一些)。如果我们自己亲自来计算呢?

我写了下面这么个脚本,可以粗略计算。为了对照方便,我同时给出了以这两种方式获取的结果。比较有趣的是,我在测试的过程中发现ps有时给出的CPU占用率结果竟然是101%!没看源代码,不知道它是怎么计算的……

BTW:无意间发现还有个dstat,貌似很酷~~8-)

[bash]

!/bin/bash

if [ $# -gt 0 ] ;
then
pid=$1
if [ ! -d /proc/${pid} ] ;
then
echo “No such process!”
exit 1
fi
else
pid=$$
fi
echo “the pid is: ${pid}”
tmp1=$(awk ‘/cpu /{print $2,$4;}’ /proc/stat)
tmp2=$(sed -e ‘s/(.*)//‘ /proc/${pid}/stat | awk ‘{print $13, $14;}’)
old_sys_stime=$(echo ${tmp1} | cut -d’ ‘ -f 2)
old_sys_utime=$(echo ${tmp1} | cut -d’ ‘ -f 1)
old_prc_stime=$(echo ${tmp2} | cut -d’ ‘ -f 2)
old_prc_utime=$(echo ${tmp2} | cut -d’ ‘ -f 1)

do some dummy stuffs

for((i=0;i /dev/null

echo “ps reported its cpu usage was: $(ps -o pcpu -p ${pid} | tail -n 1)%”

tmp1=$(awk ‘/cpu /{print $2,$4;}’ /proc/stat)
tmp2=$(sed -e ‘s/(.*)//‘ /proc/${pid}/stat | awk ‘{print $13, $14;}’)
new_sys_stime=$(echo ${tmp1} | cut -d’ ‘ -f 2)
new_sys_utime=$(echo ${tmp1} | cut -d’ ‘ -f 1)
new_prc_stime=$(echo ${tmp2} | cut -d’ ‘ -f 2)
new_prc_utime=$(echo ${tmp2} | cut -d’ ‘ -f 1)

echo -n “my calculation is:”
echo $(awk “BEGIN { print (($new_prc_stime - $old_prc_stime) + ($new_prc_utime -$old_prc_utime))/(($new_sys_stime - $old_sys_stime) + ($new_sys_utime -$old_sys_utime));}”)

#

echo “ps reported its memory usage was: $(ps -o pmem -p ${pid} | tail -n 1)%”
total=$(awk -F: ‘/MemTotal/{print $2}’ /proc/meminfo | sed -e ‘s/[^0-9]//g’)
mine=$(awk -F: ‘/VmRSS/{print $2}’ /proc/${pid}/status | sed -e ‘s/[^0-9]//g’)

echo -n “my calculation is:”
echo $(awk “BEGIN{print $mine/$total;}”)

exit 0
[/bash]

一个Bash重定向的问题

网上有人问到一个有意思的问题:如何把一个命令的输出同时重定向到3个文件?分别是:标准输出一个,标准错误输出一个,这两个都有又是一个。

我一开始以为这个不难,但我仔细想想发现不是,根本就没那么简单!首先一个问题,你重定向某个文件句柄到文件(或者另一个句柄)后意味着后面你就得不到了;其次,复制一个流我们的工具只有tee。所以,我们可以看出,这里tee至少得被调用两次才可以,因为两个流被都被复制一次。我的想法是:先tee标准输出,然后交换1和2,再tee标准错误,然后再重定向1和2到文件。

我还没来得及把想法付诸于实践,别人就有解答了,而且他的比我的还简单!看了他的我反省了一下自己的解法,第2步其实没必要交换,因为1已经被重定向过了,只需要一个临时的把它保存一下即可,下面是他的解答:

{ { your_cmd | tee stdout; } 2>&1 1>&3 | tee stderr 1>&3; } 3>stdout+err

作者说应该还有更简单的,但我是想不起来了。有知道的还望不吝赐教!(自己重写个tee不算,请用已有的命令。)

如果你觉得上面的重定向难理解,那下面这个呢?:-)

{
  {
    cmd1 3>&- |
      cmd2 2>&3 3>&-
  } 2>&1 >&4 4>&- |
    cmd3 3>&- 4>&-

} 3>&2 4>&1

关于sed的N命令

N命令很简单,就是把下一行追加到匹配空间中,我一直都是这么理解的,直到我遇到这么个问题:

~% echo -e ‘anbncndne’ | sed -ne ‘N;/$/{s/cnd/FF/;p}’
a
b
FF
~% echo -e ‘anbncndne’ | sed -ne ‘N;/$/{s/bn.*nd/FF/;p}’
a
b
c
d

按理说我每次都用N,最后得到的应该是:”anbncndne”才对啊!怎么就这么奇怪呢?所以我开始怀疑对N的理解,有人告诉我可以这么测试:

$ echo ‘a
b
c
d
e’ | sed -ne ‘N;/$/{s/.*/>&</;p}’
>a
b<
>c
d<

哈!现在问题一目了然了!出问题的不是N,N确实每次都读入了下一行,但是,sed下次再调用N时已经将下下一行读入并覆盖掉匹配空间了!所以,解决方法就是不让它替换掉!怎么办呢?一个方法就是有sed的”goto”:

$ echo -e ‘a
b
c
d
e’ | sed -ne ‘:t;N;$!bt;s/bn.*nd/FF/;p’

可见,sed很拧把却又很强大!理解这个强大确实得下一番功夫!把文本编辑的逻辑抽象成一个个sed的命令,Lee E. McMahon很牛x!同时我们也可以看出:牛人们玩的都是抽象~!而我们这些非牛们只能玩玩实现了~ :-)

关于sem_open(3)

今天新闻组上有人问到这么个问题,为什么sem_open(“/tmp/nimeni”,O_CREAT|O_EXCL,SHM_MODE,1);总是得到ENOENT?

翻开sem_open的man手册看看,我们大体就会知道,第一个参数有问题,man手册中这么说:

sem_open(3):

sem_open() creates a new POSIX semaphore or opens an existing
semaphore. The semaphore is identified by name. For details of
the construction of name, see sem_overview(7).

sem_overview(7):

Named semaphores
A named semaphore is identified by a name of the form
/somename. Two processes can operate on the same named
semaphore by passing the same name to sem_open(3).
我看了下面一个人的引用,发现我实在是没看man手册的必要了,这里的描述有问题!所以我找到了sem_open()在glibc中的实现(nptl/sem_open.c):
[c]
//….
if (mountpoint.dir == NULL)
{
__set_errno (ENOSYS);
return SEM_FAILED;
}

/ Construct the filename. /
while (name[0] == ‘/‘)
++name;

if (name[0] == ‘’)
{
/ The name “/“ is not supported. /
__set_errno (EINVAL);
return SEM_FAILED;
}
size_t namelen = strlen (name) + 1;

/ Create the name of the final file. /
finalname = (char *) alloca (mountpoint.dirlen + namelen);
mempcpy (mempcpy (finalname, mountpoint.dir, mountpoint.dirlen),
name, namelen);

/ If the semaphore object has to exist simply open it. /
if ((oflag & O_CREAT) == 0 || (oflag & O_EXCL) == 0)
{
try_again:
fd = __libc_open (finalname,
(oflag & ~(O_CREAT|O_ACCMODE)) | O_NOFOLLOW | O_RDWR);

//….
[/c]

所以这里的问题其实很简单,最根本的问题是那个path参数就究竟是用来干什么的?用一句话说,其实就是指定一个文件位置,这个位置是以tmpfs的挂载点为根目录的。也就是说,如果它以/开头,说明是从tmpfs根目录开始,而如果不是,就是从相对路径开始,不过“当前目录”也是根目录(这正是为什么sem_open()的实现中会去掉开头的/)。到了这里我们可以看出一开始那个问题的原因了,它传递的是”/tmp/nimeni”,也就假设了tmpfs的挂载目录(通常是/dev/shm)下有tmp这个目录,而事实上没有,所以即使他加上了O_CREAT|O_EXCL也会返回ENOENT。

所以,传递给sem_open()的第一个参数究竟怎么写才好呢?”/somename”和”somename”都可以,都容易读而且也都对,但是名字中间有/就不行了。

从上面我们也可以看出,对于程序员来说,有些时候啊,读文档真的还不如直接去读代码来得更快!所以这也提示我们:代码要写得像文档一样易读!我坚信好的代码就应该如此!

查找网络设备

在新闻组上看到有人问如何通过名字查找某个网络设备,其实不难,首先想到的就是通过ioctl,示例代码见下:

[c]
int get_interface(const char interface)
{
int sock = socket(PF_INET, SOCK_STREAM, 0);
struct ifconf ifc;
struct ifreq
ifr;
int ret = -1;

if (sock &lt; 0)
    return -1;

ifc.ifc_len = 0;
ifc.ifc_req = NULL;
if (ioctl(sock, SIOCGIFCONF, &amp;ifc)  0) {
    ifc.ifc_req = malloc(ifc.ifc_len);
    if (ifc.ifc_req) {
        if (ioctl(sock, SIOCGIFCONF, &amp;ifc) &lt; 0) {
            free(ifc.ifc_req);
            goto close;
        } else
            for (ifr = ifc.ifc_req;
                 (char *)ifr ifr_name);*/
                if (!strcmp(ifr-&gt;ifr_name, interface)) {
                    ret = 0;
                    break;
                }
            }
    } else
        return -1;

    free(ifc.ifc_req);
}

close:
close(sock);
return ret;
}
[/c]

然后有人说POSIX其实还有个if_nameindex(),它明显要比ioctl可移植性要高,用它重写上面的代码如下:
[c]
int get_interf(const char *interf)
{

int ret = -1;
struct if_nameindex *ifp, *ifpsave;
ifpsave = ifp = if_nameindex();

if (!ifp)
    return -1;

while (ifp-&gt;if_index) {
    if (strcmp(ifp-&gt;if_name, interf) == 0) {
        ret = 0;
        break;
    }
    ifp++;
}

if_freenameindex(ifpsave);
return ret;

}
[/c]

我查了一下glibc中的实现,发现在linux上其实如果不用netlink的话,if_nameindex()也就是对ioctl(…SIOCGIFCONF…)的一个包装,具体可参考sysdeps/unix/sysv/linux/if_index.c。netlink这个东西够奇怪的,在这里居然也能派上用场,有机会要研究研究~

C中的宏预处理

宏的预处理这个坑看起来浅,其实还蛮深的。它也是最容易被忽视的几个地方之一。这里斗胆来谈谈,说实话,在写这句话时我也不清楚这坑究竟有多深,没关系,我们摸着石头过河,一起看看到最后这坑能有多深!同时这篇文章也将会是《C语言编程艺术》中的一部分。

从一个相对简单的例子说起吧。
[c]

define f(a,b) a##b

define g(a) #a

define h(a) g(a)

h(f(1,2))
g(f(1,2))
[/c]
相信不少人都见过这个例子。我们不妨再仔细分析一下它的解析过程。应该是这样的:

对于g(f(1,2)),预处理器看到的先是g,然后是(,说明这是一个函数式的宏,好,然后替换后面的实参f(1, 2),得到#f(1,2)(注:直接这么写非法,这里只是为了表示方便而已),因为它前面有个#,所以下一步是不会替换f的参数的!所以进一步得到”f(1, 2)”,解析结束。而对于h(f(1,2)),预处理器看到的先是h,然后(,对其参数f(1, 2)进行替换,得到g(f(1,2)),注意这里的下一步是,预处理器就继续往后走,处理刚得到的f(1,2),而不是回过头去再处理g!得到12,到了这里我们的得到的是一个:g(12),然后重新扫描整个宏,替换g,最后得到”12”。

标准第6.10.3.1节中对此描述的还比较清晰,它这样写道:

After the arguments for the invocation of a function-like macro have been
identified, argument substitution takes place. A parameter in the replacement
list, unless preceded by a # or ## preprocessing token or followed by a ##
preprocessing token (see below), is replaced by the corresponding argument
after all macros contained therein have been expanded.
注意加粗的部分。到了在这里,我们可以简单总结一下函数式宏的基本替换流程:

首先要识别出这是一个函数式宏,通过什么?通过调用中出现的(,没错是左括号!到这里后下一步是参数替换,就是根据该宏的定义把实参全部替换进去,然后接着向后走,除非是遇到了#和##(正如上面例子中的g),把后面替换后的东西中如果还有已知宏的话,进行替换或者同样的展开,直到解析到末尾:所有的参数都已经替换完(或者#或##已经处理完);最后,预处理器还会对整个宏再进行一次扫描,因为前一轮替换中有可能在前面替换出一些新的东西来(比如上面例子中的h)。

这里咋看之下没什么问题,其实问题很多!为什么?因为宏替换不仅允许发生在“调用”宏的时候,而且还发生在它定义时!

问题1:宏的名字本身会被替换吗?

这个问题也可以这样问:宏允许被重新定义吗?不允许,但是允许相同的重新定义。标准这样写道:

An identifier currently defined as an object-like macro shall not be
redefined by another #define preprocessing directive unless the second definition
is an object-like macro definition and the two replacement lists are identical.
Likewise, an identifier currently defined as a function-like macro shall not be
redefined by another #define preprocessing directive unless the second definition
is a function-like macro definition that has the same number and spelling of
parameters, and the two replacement lists are identical.

问题2:宏的参数(形参)会被替换吗?

先举个例子说明这个问题:

define foo 1

define bar(foo) foo + 2

bar(a)

我们是得到a+2还是1+2?a+2!因为形参是不会被替换掉的,你想想啊,如果形参都被替换掉了这个宏就没什么作用了!那实参呢?实参会的,因为实参的替换发生在传递这个参数之前:

Before being substituted, each argument’s preprocessing tokens are
completely macro replaced as if they formed the rest of the preprocessing file

问题3:宏中参数之外的符号会被替换吗?

会,上面提到过“after all macros contained therein have been expanded”,也就是说这个发生在参数替换之前。但是,这里有个非常诡异的问题:如果被替换出来的符号正好和形参一样怎么办?就像下面这个例子:

define foo bar

define baz(bar) bar + foo

baz(1)

我们会得到1+1还是1+bar?后者,因为替换出来的那个bar是不会计算在形参之内的,虽然标准并没有明确这一点。想想吧,如果是的话那个宏的定义也会被破坏了!

另一个例子:

define foo bar

define mac(x) x(foo)

mac(foo)

根据上面所说,我们首先得到foo(foo),然后foo再被替换成bar,最后得到bar(bar)。

好了,到这里我们终于可以看一下更复杂的例子了:

define m !(m)+n

define n(n) n(m)

m(m)

这个例子相当复杂,是我见过的最复杂的一个宏。:-) 刚看到我们可能都有点蒙,没关系,咱们一步一步地来。

第一步很好走,第一个m直接被替换,得到:!(m)+n(m),别犹豫,接着往下走,替换最后一个m,得到:!(m)+n(!(m)+n),这时这一遍扫描已经完成。到这里我们得提出另外一个东西才能继续,你可能知道,递归。标准对此的描述是:

If the name of the macro being replaced is found during this scan of the
replacement list (not including the rest of the source file’s preprocessing
tokens), it is not replaced.

在上次替换中,被替换的是m,所以m在这里的再次出现将不会被替换,所以下一步是会替换第一个n,得到:!(m)+!(m)+n(m),注意这里又替换出一个新的m来,这个m会被替换,因为这次扫描还没完成!下一步得到:!(m)+!(m)+n(!(m)+n),第二遍扫描结束,全部的替换完成。

综上,我们可以总结出两条重要的宏替换规则:1)再复杂的宏也只是被扫描两遍,而且递归是不允许发生的,即使在第2遍时;2)一个替换完成后如果还没扫描完,要从被替换的那里继续。

(全文完)

还谈数组

作者:西邮 王聪

《C专家编程》中曾两次谈到数组,可是我觉得还不够,仍然没有把数组的本质给说出来。这里我想来个了断,终止一切关于数组和指针关系的讨论。:-)

为了先让你困惑一下,先看下面的程序:
[c]

include

typedef int color[3];

size_t array_size(color a)
{
return sizeof(a);
}
size_t array_size_p(color a)
{
return sizeof(
a);
}

int main(void)
{
color c = {0,};
color *p = &c;

    c[1] = 10;
    *(c+1) = 11;
    printf("%dn", c[1]);
    printf("%dn", sizeof(c));
    printf("%dn", array_size(c));
    printf("%dn", array_size_p(p));

    printf("p=%lxn", (unsigned long)p);
    printf("p+1=%lxn", (unsigned long)(p+1));

    return 0;

}
[/c]

如果你看了上面的程序没感到困惑,那说明:要么你完全理解这里的问题;要么你根本就不懂数组和指针的关系,而且根本就没仔细想过,只是知道结果罢了。

如果你去参考《C专家编程》,你得到的是分情况的讨论,一条一条对应得很好,如果你照这样理解的话(就像我以前一样),也感觉不出不舒服来。但是,它没有点破本后的本质,至少我是没看到。我在这里得说一下。这里的问题究竟是什么?问题出在对数组的定义上,理解这里后面的一切都好办了。数组的定义是什么?太简单了,无非就是:

int a[10];

别小看这里,这里太关键了,以至于很多人都给忽略了。这里究竟发生了什么?这里定义了一个长度为10的int数组,其名字叫作a。这似乎是废话,但你琢磨琢磨这里的词,我是把名字和数组分开说的,这就是关键!!进一步说是,a仅仅是个名字,它并不是指向数组第一个元素的整型指针,也不是一个数组类型!更为关键的是,它在内存中根本就不存在!!这样问题就来了,那a怎么还和数组第一个元素的值相同呢?只是碰巧相同罢了!这好比“解放路1号”和“解放路”的地址相同!如果给a下个定义,那它是什么?我觉得,可以这么定义a:它是一个符号,语法上的;如果我们要它的内容,它就代表了其整个数组;如果我们只要它的值,那么它就是数组的起始地址。

我们把上面的定义一段一段解剖来看。“它是一个符号,语法上的”,这可以很好地解释为什么数组名是个常量,它出现的地方都是在语法上进行了适当的替换,至于如何替换,就是下面两条了。

“如果我们要它的内容,它就代表了其整个数组”,这句可以很好地解释下面的例子:
[c]
color *p = &c;
[/c]
这里取址的意义是“我取c代表的整个数组的地址”。这也可以解释sizeof为什么在函数外面作用于数组时取到的是整个数组的大小。所以,说“数组名就是指向其第一个元素的指针”是错误的!

“如果我们只要它的值,那么它就是数组的起始地址”,这句可以很好地解释为什么数组作为函数参数传递时会是指针,因为在函数参数使用其地址明显要比使用其全部内容要快。这也可以解释c[1]存在的道理,因为这里取的也是它代表的地址。所以,说“数组名是数组类型”也是不对的!(如果真是的话,那么它+1和上面例子中的p+1应该是一样的才对。)

综上,数组名什么都是,它是两者的混合:当它表现出内容特性时,它的类型是数组类型;当它表现出地址特性时,它的类型又是其指向第一个元素的指针类型。它也什么都不是,它只是个名字,它在内存中本身是不存在的,存在的是其背后实实在在的数组,它只是它们的一个语法上的代表罢了。这也就决定了从语法上你根本就不可能直接去定义一个数组类型的变量,你只能定义一个指向数组类型的指针,然后间接地去找到背后那个数组类型!

P.S. 因此,在C语言中,你根本就不可能在函数中直接去看到一个数组参数的真实类型,而C++可以,多亏了引用!看下面的例子:
[cpp]

include

using namespace std;
template
size_t array_size(T & ar)
{
return sizeof(ar) / sizeof(ar[0]);
}
int main()
{
int ar[100] = {0};
cout<<array_size(ar)<<endl;

    return 0;

}

[/cpp]

你看,语法上的问题也只能在语法上解决!这叫什么来着?“以其人之道还治其人之身”!

以上纯属一家之言,读后请自己三思!!