《C专家编程》阅读笔记

12 篇文章 0 订阅

一、符号重载

 

符号

意义

static

在函数内部,表示该变量的值在各个调用间一直保持延续性

在函数这一级,表示该函数只对本文件可见

extern     

用于函数定义,表示全局可见(属于冗余的)

用于变量,表示它在其他地方定义

void 

作为函数的返回值类型,表示不返回任何值

在指针声明中,表示通用的指针类型

位于参数列表中,表示没有参数

*

乘法运算符

用于指针,间接引用

在声明中,表示指针

&

位的AND操作符

取地址操作符

=

赋值符

==

比较运算符

<=

<<=

小于或等于运算符

左移复合赋值运算符

<

小于运算符

#include 指令的左定界符

()

在函数定义中,包围形式参数表

调用一个函数

改变表达式的运算次序

将值转换为其他类型(强制类型转换)

定义带参数的宏

包围sizeof操作符的操作数(如果它是类型名)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

二、运算符优先级

C语言运算符优先级存在的问题

优先级问题

表达式

人们可能误以为的结果

实际结果

.的优先级高于*

->运算符用于消除这个问题

*p.f

p所指向的字段f (*p).f

对p取f偏移,作为指针,然后进行解除引用操作 *(p.f)

[ ]高于*

int *ap[]

ap是一个指向int数组的指针

int (*ap)[]

ap是个元素为int指针的数组,int*(ap[])

函数()高于*

int *fp()

fp是个函数指针,返回值指向int,int(*fp)()

fp是个函数,返回int*,int *(fp())

==和!=高于位运算符

(val & mask != 0)

(val & mask) != 0

val &(mask !=0)

==和!=高于赋值运算符

c = getchar() != EOF

(c = getchar()) != EOF

c = (getchar() != EOF)

算术运算符高于移位运算符

msb << 4 + lsb

(msb << 4) + lsb

msb << (4 + lsb)

逗号运算符在所有运算符中优先级最低

i = 1,2

i = (1,2)这样的话i的结果是2而不是1

(i = 1),2最终结果i的值是1而不是2

 

 

 

 

 

 

 

 

三、声明

1、声明的优先级

A.声明从它的名字开始读取,然后按照优先级顺序依次读取

B.优先级从高到低依次是:

  B.1 声明中被括号括起来的部分

  B.2 后缀操作符:

        括号()表示这是一个函数,而方括号[]表示这是一个数组

  B.3 前缀操作符:

        星号*表示"指向...的指针"

C.如果const和(或)volatile关键字的后面紧跟类型说明符(如int,long等)那么它作用于类型说明符。在其他情况下,const和(或)volatile关键字作用于它左边紧邻的指针星号.

2、常见声明

声明

含义

char (*j)[20]

j = (char (*)[20]) malloc(20)

j是一个指向数组的指针,数组内有20个char元素

char *j[20]

j是一个指针数组,数组内有20个指向char *类型的指针

int (* fun())()

函数fun的返回值是一个函数指针

int (* fun())[]

函数fun的返回值是一个指向数组的指针

int (* foo[])()

foo是一个数组,每个元素为一个函数指针

const int * pInt

pInt 所指向的对象是只读的

int const * pInt

pInt 所指向的对象是只读的

int * const pInt

pInt指针是只读的

const int * const pInt

pInt指针和pInt 所指向的对象都是只读的

int const * const pInt

pInt指针和pInt 所指向的对象都是只读的

char * const *(*next)()

next是一个指向函数的指针,该函数返回另一个指针,该指针指向一个只读的指向char的指针

char *(* c[10])(int **p)

c是一个数组,它的元素类型是函数指针,其所指向的函数的返回值是一个指向char的指针。

该函数的唯一参数是指向int指针的指针

(*(void(*)())0)()

void(*)()是一个函数指针,该函数返回void型数据
(void(*)())0就是将常数0转换为指向返回值为void的函数的指针
(*(void(*)())0)();就是一个表达式,调用0地址的函数

该表达式相当:
typedef void (*funcptr)();
(*(funcptr)0)();

int *(* (*fun)(int))[10];fun为一个函数的指针,函数有一个int类型参数。该函数返回指向指针数组的指针。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

3、声明与定义

变量定义:
所谓定义就是编译器创建一个对象,并且为变量分配一块存储空间,并给它取上一个名字,这个名字就是我们经常所说的变量名或对象名。在程序中对象有且仅有一个定义。例如int a 在声明的时候已经建立了存储空间。

变量声明:声明有两重含义:

第一重含义:告诉编译器,这个名字已经匹配到一块内存上了,下面的代表用到该变量或对象是在别的地方定义的,声明可以出现多次。
第二重含义:告诉编译器,这个名字我先预定了,别的地方再也不能用它作为变量名或对象。
定义与声明最重要的区别是:
定义创建了对象并为这个对象分配了内存,声明没有分配内存;例如extern int a其中变量a是在别的文件中定义的。

思考一个问题:为什么会出现对象的声明?
声明的最终目的是为了提前使用,即在定义之前使用,如果不需要提前使用,就没有单独声明的必要了。

 

声明相当于普通的声明:它所说明的并非自身,而是描述替他地方创建的对象,声明可以多次出现。

定义相当于特殊的声明:它为对象分配内存,只能出现一次。

 

1.在一个程序中只能对变量定义一次,因为我们不能让编译器一直为同一个变量,函数分配不同的存储空间;而可以对变量进行很多次的声明。

2.在任何多文件中使用的变量都需要有与定义分离的声明。在这种情况下,一文件含有变量的定义,则使用该变量的其他文件中就要含有该变量的声明,而不是定义。

3.在头文件中不能放变量的定义,一般存放变量的声明。因为头文件要被其他文件包含,如果放到头文件当中就不能避免变量被多次定义。(除了const,inline、类定义外)

三个例外:1)值在编译时就已知的const变量的定义可放在头文件中,如:const int num=10;
          2)类的定义可放在头文件中。
          3)inline函数。

一般头文件中只是放变量的声明,因为头文件要被其他文件包含#include,如果把定义放在头文件的话,就不能避免多次定义变量。不允许多次定义变量,一个程序中对指定变量的定义只有一次,声明可以无数次。

下面的语句是定义,不能放在头文件中:
extern  int ival=10; //虽然ival声明为extern,但是它初始化了,代表这是个定义。
double fica_rate;  //fica_rate虽然没有初始化,但是没有extern。所以仍是定义。

 

只有全局变量并且没有被static声明的变量才能声明为extern。所以,如果你不想自己源文件中全局变量被其他文件引用,你就可以给变量加上static声明。这里说的是非const的全局变量,如果是const的全局变量,如果想被其他文件访问,需要在定义时,加上extern关键字,表示它可被其他文件声明使用的。否则的话,这个变量只能在它被定义的文件里面被访问,其他文件不能访问。为什么非const变量定义时候没有extern,因为非const变量默认为extern,const变量默认为文件的局部变量。而const变量如果想在其他文件里被访问,必须显示的指定它为extern。

static全局变量是有文件作用域的。在a.c中用了,在其他文件中就不能使用了。static变量一般放在.cpp或者.c文件中。不放在.h文件中。

 

只有字符串常量才可以初始化指针数组,指针数组不能由非字符串的类型直接初始化.

char *vegetables[] = {"carror", "celery", "corn"} /* 没问题 */

int *weights[] = {{1,2,3,4,5},{6,7},{8,9,10}}; /* 无法成功编译 */

4、extern

extern用于声明变量或者函数是一个外部变量,也就是告诉编译器这个变量或者函数是在别的文件中定义的,编译的时候不要报错,在链接的时候按照字符串寻址可以找到这个变量或者函数。声明extern关键字的全局变量和函数可以使得它们能够跨文件被访问。全局函数的声明语句中,关键字extern可以省略,因为全局函数默认是extern类型的。

extern修饰变量的声明
extern将变量的能见度延伸到了整个程序,我们知道变量声明和定义的地方,可以在整个程序的任何地方使用他们。
 

extern修饰函数的声明

默认情况下,声明和定义函数,都有一个extern的前缀,这意味着声明和定义函数时,前面不写extern,它也是默认存在的。

 

与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern 修饰。

5、static

1、变量类型

C语言中对变量的说明包括两方面的内容:变量类型以及变量的存储类型。变量类型如:int(整形),char(字符型)是用来说明变量所占用的内存空间的大小。变量存储类型用来说明变量的作用范围。
C语言的变量存储类型有:自动类、寄存器类、静态类和外部类。

局部变量
是指在函数内部说明的变量(有时也称为自动变量),用关键字auto进行说明。 所有的非全局变量都被认为是局部变量,所以auto实际上从来不用。局部变量在函数调用时自动产生,但不会自动初始化, 随函数调用的结束,这个变量也就自动消失了,下次调用此函数时再自动产生,还要重新赋值,退出时又自动消失。

静态变量
是用关键字static声明。根据变量的类型可以分为静态局部变量和静态全程变量。

(1)静态局部变量
它与局部变量的区别在于:在函数退出时,这个变量始终存在,但不能被其它函数使用,当再次进入该函数时,将保存上次的结果。其它与局部变量一样。
(2)静态全局变量
静态全局变量就是指只在定义它的源文件中可见而在其它源文件中不可见的变量。它与全局变量的区别是:全局变量可以再说明为外部变量(extern),被其它源文件使用,而静态全局变量却不能再被说明为外部的,即只能被所在的源文件使用。
(3)静态变量生命周期
静态变量在程序运行之前创建,在程序的整个运行期间始终存在,直到程序结束。
外部变量
是用关键字extern声明。为了使变量除了在定义它的源文件中可以使用外,还可以被其它文件使用,就要将全程变量通知每一个程序模块文件,此时可用 extern来说明。

寄存器变量
通常在比较注重在执行速度的情况下使用。其思想是告诉编译程序把该变量放在某个CPU寄存器中。因为数据在寄存器中操作比在内存中快,这样就提高了程序代码的执行速度。寄存器变量的说明是在变量名及类型之前加上关键字register。值得注意的是取地址运算符&不能作用于寄存器变量。


3.2 static修饰变量


static的第一个作用是:修饰变量。变量分又分局部变量和全局变量,但它们都存在内存的静态区。
静态全局变量,作用域仅限于变量被定义的文件中,其他文件即使用extern声明也没法使用他。准确的说作用域是从定义除开始,到文件结尾处结束,在定义处前面的那些代码行均不能使用它。
静态局部变量,在函数体里面定义的,就只能在这个函数用了,同一个源程序中的其他函数也用不了。由于被static修饰的变量总是存在内存的静态区,所以即使这个函数运行结束,这个静态变量的值还是不会被销毁,函数下次使用时仍然能用到这个值。

static的第二个作用是:修饰函数。函数前面加static使得函数称为静态函数,区别于非静态函数(外部函数)。但此处“static”的含义不是指存储方式,而是指函数的作用域仅局限于本文将(所以又称为内部函数)。使用内部函数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其他文件中的函数同名。

6、extern 与const


const修饰的全局常量具有跟static相同的特性,即它们只能作用于本编译模块中,且static修饰的是全局变量,但是const可以与extern连用来声明该常量可以作用于其他编译模块中,如externconst char g_str[];

然后在原文件中别忘了定义:const char g_str[] = “123456”;

所以当const单独使用时它就与static相同,而当与extern一起合作的时候,它的特性就跟extern的一样了!所以对const我没有什么可以过多的描述,我只是想提醒你,const char* g_str = “123456” 与 const char g_str[] ="123465"是不同的,前面那个const修饰的是char 而不是g_str,它的g_str并不是常量,它被看做是一个定义了的全局变量(可以被其他编译单元使用), 所以如果你想让char g_str遵守const的全局常量的规则,最好这么定义const char* const g_str=“123456”。
 

 

四、typedef与define

 

typedef可以把几个声明器塞到一个声明中去,但不建议这样做。

 

void (*signal(int sig, void (*func)(int)))(int);

signal是一个函数,它返回一个函数指针,后者所指向的函数接收一个int参数并返回void。signal有两个参数,一个是int型的参数sig,另一个是函数指针*func,*func指向的函数接收一个int参数,返回值为void。

typedef void (*ptr_to_func)(int);  //表示ptr_to_func是一个函数指针,指向的函数接收一个int参数,返回void。

ptr_to_func signal(int, ptr_to_func);   //与void (*signal(int sig, void (*func)(int)))(int);完全相同

 

typedef为数据类型创建别名,而不是创建新的数据类型。

typedef int *ptr, (*fun)(), arr[5]; 

/* ptr是“指向int的指针”类型;fun是“指向返回值为int的函数的指针”类型;arr是“长度为5的int型数组”类型。*/

 

1、typedef是一种彻底的封装类型——在声明它之后不能再往里面增加别的东西,可以用其它类型说明符对宏类型名进行扩展,但对typedef所定义的类型名却不能这样做。

#define peach int;

unsigned peach i; /*没问题*/

typedef int banana;

unsigned banana i /*错误,不能添加unsigned*/

2、在连续几个变量声明中,用typedef定义的类型能够保证声明中所有的变量均为同一种类型,而用#define定义的类型则无法保证。

#define int_ptr int *

int_ptr chalk, cheese;

经过宏扩展,第二行变为:

int * chalk,cheese;  //chalk是一个指向int的指针,而cheese则是一个int变量

typedef int * int_ptr;

int_ptr chalk, cheese;  // chalk和cheese均为指向int的指针。

 

typedef struct fruit { int weight, price;} fruit;  //声明结构标签“fruit”和由typedef声明的结构类型“fruit”

struct fruit mandarin; //使用结构标签“fruit”

fruit mandarin;  //使用结构类型“fruit”

struct veg{ int weight , price} veg;  //声明结构标签“veg”和变量“veg”

struct veg potato;  //使用结构标签“veg”

veg potato;  //错误,不能这样声明

五、数组与指针

1、数组与指针的区别

数组与指针的访问过程:

char a[9] = “abckefg”;   c = a[i];

编译器符号表具有一个地址9980

              运行时步骤1:取i的值,将它与9980相加;

              运行时步骤2:取地址[9980+i]的内容。

Char *p;   c = *p;

编译器符号表有一个符号p,它的地址是4624

              运行时步骤1:取地址4624的内容,就是’5081’;

              运行时步骤2:取地址5081的内容。

 

定义为指针,但以数组方式引用的过程:

char *p = “abchdefs”                                        c = p[i]

编译器符号表有个p,地址为4624

              运行时步骤1:取地址4624的内容,即’5081’;

              运行时步骤2:取得I的值,并将它与5081相加;

              运行时步骤3:取地址[5081+i]的内容。

 

声明如下:

char  p[9] = “abcdefgh”

char c = p[3] //p是一个数组也是一个内存地址,此时编译器符号表有p的内存地址,运行时步骤

1:取偏移量3与内存地址p相加

2:取地址(p+3)的内容

当书写了extern char *p,然后用p[3]来引用其中的元素时,编译器将会:

(1) 取得符号表中p的地址,提取存储与此处的指针

(2) 把下标所表示的偏移量与指针相加,产生一个地址

(3) 访问上面的地址,取得字符

既然把p声明为指针,那么不管p原先是定义为指针还是数组,都会按照上面所示的三个步骤进行操作,但是只有当p原来定义为指针时这个方法才是正确的.

p在这里声明为extern char *p;而它原先定义的却是char p[9];这种情形.当用p[i]这种形式提取这个声明的内容时,实际上得到的是一个字符.但按照上面的方法,编译器却把他当成一个指针,ACSII字符解释为地址显然是牛头不对马嘴。

指针

数组

保存数据的地址

保存数据

间接访问数据,首先取得指针的内容,把它作为地址,然后从这个地址提取数据.

如果指针有一个下标[I],就把指针的内容加上I作为地址,从中提取数据

直接访问数据,a[I]只是简单的以a+I为地址取得数据

通常用于动态数据结构

通常用于存储固定数目且数据类型相同的元素

相关的函数为malloc(),free()

隐式分配和删除

通常指向匿名数据

自身即为数据名

 

 

 

 

 

 

 

 

定义指针时,编译器并不为指针所指向的对象分配空间,它只分配指针本身的空间,除非在定义时同时赋给指针一个字符串常量进行初始化.例如,下面的定义创建了一个字符串常量(为其分配了内存):

char *p = "breadfruit";

注意只有对字符串常量才是如此.不能期望为浮点数之类的常量分配空间,如:

float *pip = 3.141; /* 错误!无法通过编译. */

在ANSI C中,初始化指针时所创建的字符串常量被定义为只读.如果试图通过指针修改这个字符串的值,程序就会出现未定义的行为.在有些编译器中,字符串常量被存放在只允许读取的文本段中,以防止它被修改.

数组也可以用字符串常量进行初始化:

char a[] = "gooseberry";

与指针相反,由字符串常量初始化的数组是可以修改的.其中的单个字符在以后可以改变.比如下面的语句:

strncpy(a,"black",5);

 

下面总结一下数组的数组名的问题。声明了一个数组TYPE array[n],则数组名称array就有了两重含义:第一,它代表整个数组,它的类型是TYPE[n];第二 ,它是一个指针,该指针的类型是TYPE*,该指针指向的类型是TYPE,也就是数组单元的类型,该指针指向的内存区就是数组第0号单元,该指针自己占有单独的内存区,注意它和数组第0号单元占据的内存区是不同的。该指针的值是不能修改的,即类似array++的表达式是错误的。
在不同的表达式中数组名array可以扮演不同的角色。在表达式sizeof(array)中,数组名array代表数组本身,故这时sizeof函数测出的是整个数组的大小。
在表达式*array中,array扮演的是指针,因此这个表达式的结果就是数组第0号单元的值。sizeof(*array)测出的是数组单元的大小。
表达式array+n(其中n=0,1,2,....。)中,array扮演的是指针,故array+n的结果是一个指针,它的类型是TYPE*,它指向的类型是TYPE,它指向数组第n号单元。故sizeof(array+n)测出的是指针类型的大小。
 

2、再论数组

规则1: 表达式中的数组名(与声明不同)被编译器当作一个指向该数组第一个元素的指针.

int a[10], *p, i=2;

可以通过以下任何一种方法访问a[i];

p = a; p[i];

p = a; *(p+a);

p = a + i; *p

在表达式中,指针和数组是可以互换的,因为它们在编译器里的最终形式都是指针,并且都可以进行取下标操作。

 

编译器自动把下标值的步长调整到数组元素的大小,对起始地址执行加法操作之前,编译器会负责计算每次增加的步长,这就是为什么指针总是有类型限制,每个指针只能指向一种类型的原因所在——因为编译器需要知道对指针进行解除引用操作时应该取几个字节,以及每个下标的步长应取几个字节。

 

规则2: C语言把数组下标作为指针的偏移量

 

规则3: “作为函数参数的数组名”等同于指针

在函数参数的声明中,数组名被编译器当作指向该数组第一个元素的指针.

在函数形参定义这个特殊情况下,编译器必须把数组形式改写成指向数组第一个元素的指针形式。编译器只向函数传递数组的地址,而不是整个数组的拷贝。因此在my_function()的调用上,无论实参是数组还是真的指针都是合法的,下列三种形式是完全等同的。

my_function(int *turnip) { ... }

my_function(int  turnip[]) { ... }

my_function(int  turnip[200]) { ... }

 

有一种操作只能在指针里进行而无法在数组中进行,那就是修改它的值.数组名是不可修改的左值,它的值是不能改变的.

 

数组和指针可交换性的总结

(1) 用a[i]这样的形式对数组进行访问总是被编译器"改写"或解释为像*(a+1)这样的指针访问。

(2) 指针始终是指针。它决不可以改写成数组。你可以用下标形式访问指针,一般都是指针作为函数参数时,而且你知道实际传递给函数的是一个数组。

(3) 在特定的上下文中,也就是它作为函数的参数(也只有这种情况),一个数组的声明可以看作是一个指针。作为函数参数的数组(就是在一个函数调用中)始终会被编译器修改成为指向数组第一个元素的指针。

(4) 因此,当把一个数组定义为函数的参数时,可以选择选择把它定义为数组,也可以定义指针。不管选择哪种方法,在函数内部事实上获得的都是一个指针。

(5) 在其他所有情况中,定义和声明必须匹配。如果定义了一个数组,在其他文件对它进行声明时也必须把它声明为数组,指针也是如此。

3、再论指针

1、多维数组

1.pea[i][j]将被编译器解析为: *(*(pea + i) + j)

 

2.数组和指针参数是如何被编译器修改的

"数组名被改写成一个指针参数"规则并不是递归定义的。数组的数组会被改写为"数组的指针",而不是"指针的指针".

实参

所匹配的形式参数

数组的数组

char c[8][10];

char (*)[10];

数组指针

指针数组

char *c[15];

char **c

指针的指针

数组指针(行指针)

 

char (*c)[64];

 

char (*c)[64];

 

不改变

指针的指针

 

char **c

 

char **c

不改变

 

 

 

 

 

 

 

 

 

在main()函数中看到char **argv这样的参数,是因为argv是一个指针数组(char * argv[])。这个表达式被编译器改写成为指向数组第一个元素的指针,也就是一个指向指针的指针。如果argv参数事实上被声明为一个数组的数组(例如char argv[10][15]),它将被编译器改写为char (* argv)[15],也就是一个字符数组指针,而不是char **argv。

 

下面3个函数都接收同样类型的参数,3个变量apricot、p、*q都匹配所有3个函数的参数声明。

my_function_1( int fruit[2][3][5]) { ; }

my_function_1( int fruit[][3][5]) { ; }

my_function_1( int (*fruit)[3][5]) { ; }

int apricot[2][3][5];

int (*p)[3][5] = apricot;

int (*q) [2][3][5] = &apricot;

 

2、向函数传递数组

向函数传递一个一维数组

任何一维数组均可以作为函数的实参。形参被改写为指向数组第一个元素的指针,所以需要一个约定来提示数组的长度。

  1. 增加一个额外的参数,表示元素的数目(argc就是起这个作用)
  2. 赋予数组最后一个元素一个特殊的值,提示它是数组的尾部(字符串结尾的‘\0’字符就是起这个作用),这个特殊值必须不会作为正常的元素值在数组中出现。

C语言无法表达“这个数组的边界在不同的调用中可以变化”这个概念。C语言必须知道数组边界,从而为下标引用产生正确代码。

二维或多维数组无法在C语言中作一般的形参。无法向函数传递一个普通的多维数组。

可以向函数传递预先确定长度的特殊数组。

对于二维数组,数组被改写为指向数组第一行的指针。现在需要两个约定,其中一个用于提示每行的结束,另一个用于提示所有行的结束。提示单行结束可以使用一维数组所用的两种方法,提示所有行结束也可以这样。我们所接收的是一个指向数组第一个元素的指针。每次当对指针执行自增操作时,指针就指向数组中下一行的起始位置,但怎么知道指针到达了数组的最后一行呢?我们可以增加一个额外的行,行内所有元素的值都是不可能在数组正常元素中出现的,能够提示数组超出了范围。当对指针进行自增操作时,要对它进行检查,看看是否到达了那一行。另外一种方法是,定义一个额外的参数,提示数组的行数。

我们能够采取的最好方法就是放弃传递二维数组,把array[x][y]这样的形式改写为一个一维数组array[x+1],它的元素类型是指向array[y]的指针。这样就改变了问题的性质。在数组最后的那个元素array[x+1]里存储一个NULL指针,提示数组的结束。

我们知道,在C语言中无法传递一个普通的多维数组。

    这是因为我们需要知道每一维的长度。以便在地址运行的时候提供正确的长度单位。在C语言中,我们没有办法在实参和形参之间交流这种数据。因此我们必须提供除了最左边一维以外的所有维的长度信息,不然就会出错。

例如调用函数如下:

void my_function(int array[][3][5]){;}

我们可以按照下列的方式调用:

int arr[100][3][5];   my_function(arr); //OK

int arr[10][3][5];    my_function(arr);  //OK

但是像下面的就不行:

int arr[100][33][24]; my_function(arr); //ERROR

int arr[10][4][6];    my_function(arr);    //ERROR

这将无法通过编译器。

 

那么怎么来传递一个多维数组呢?

方法一:定义void my_function(int array[3][5])

这样最简单,但是作用也是最小的,因为它只能处理3行5列的数组数据。其实多维数组的最左边的一维长度可以省略,因为函数知道了其他维的信息,它就可以一次跳过一个完整的行,到达下一行。

 

方法二:void my_function3(int array[][5])。

这也就是方法一中提到的省略最左边第一维的数据。这样的做法表示每一行都必须正好是5个整数的长度。函数也可以类似的声明:

void my_function3(int (*array)[5])

  其中的括号是必须需要的,确保它是一个指向5个int类型数组的指针,而不是一个5个int *类型元素的数组。

 

方法三:放弃传递二维数组。也就是说创建一个一维数组,数组中的元素是指向其他东西的指针。

回想一下前面介绍的main函数里,我们已经习惯了char *argv[]参数的形式,有时候我们也能看见,char **argv的形式。所以我们可以简单地传递一个指向数组参数的第一个元素的指针,如下:

void my_function3(int **array);

注意:只有把二维数组改为一个指向向量的指针数组的前提下才可以这样做。

它允许任意的字符串指针数组传递给函数,但必须是指针数组,而且必须是指向字符串的指针数组。这是因为字符串和指针都有一个显示的越界值(分别为NUL和NULL),可以作为结束标记。至于其他类型,并没有一种类似的通用且可靠的值。所以并没有一种内置的方法知道何时到达数组某一维的结束为止。即使是指向字符串的指针数组,通常也需要一个计数参数argc,记录字符串的数量。

 

方法四:采用迂回的方法:char array[i*columns+j];

   因为数组的存储都是连续的线性的特点,所以我们可以将多维数组降维处理。因为 C语言是数组的数组,所以我们可以将多维数组降为一维数组,这样我们处理起来就比较方便了。

  毕竟函数中数组的传递总是会被编译器将其翻译为指针,这样我们正好可以利用这一特性,有些时候可以简化多维数组的操作,(并不代表全部)。

总结:如果多维数组各维的长度是一个完全相同的固定值,那么把它传给一个函数毫无问题。如果情况更一般些,也更常见一些,就是作为函数的参数的数组的长度是任意的,分析如下:

1,  一维数组——没有问题,但须包括一个计数值或者是一个能够标识越界位置的结束符。被调用的函数无法检测数组参数的边界。

2,  二维数组——不能直接传递给函数,但可以把矩阵改写为一个一维的指针数组,并使用相同的下标表示法。对于字符串来说,这样是可以的,对于其他类型,需要增加一个计数值或者能够标识越界位置的结束符。同样,他依赖于调用函数和被调用函数之间的约定。

3,  三维或更多维——都无法使用。必须把它分解为几个维数更少的数组。

3、使用指针从函数返回一个数组

无法直接从函数返回一个数组,但是可以让函数返回一个指向任何数据结构的指针,当然也可以是一个指向数组的指针。

但不能返回一个指向函数的局部变量的指针.

 

4、使用指针创建和使用动态数组

当需要在动态表中增长一个项目时,可以进行如下操作:

(1) 对表进行检查,看看它是否真的已满

(2) 如果确实已满,使用realloc()函数扩展表的长度.并进行检查,确保realloc()操作成功进行.

(3) 在表中增加所需要的项目.

用C代码表示,大致如下:

int current_element = 0;

int total_element = 128;

char *dynamic = malloc(total_element);

 

void add_element(char c)

{

    if(current_element == total_element - 1)

    {

       total_element *= 2;

       dynamic = (char *)realloc(dynamic, total_element);

       if(dynamic == NULL) error("Couldn't expand the table");

    }

    current_element++;

    dynamic[current_element] = c;

}

4、例子

例一:

int array[12][31];
/*
数组的嵌套定义,array是一个含有12个数组类型元素的数组,其中的每一个
元素是一个int数组,array的类型是int[12][31],其指向的类型是int[31],可以理解为array是指向数组的指针。
array[1]是一个含有31个int型元素的数组,它的类型是int[31],它指向的类型是int;
*/

int I;
int *p; 
int **q;  
int (*ptr1)[31];  
int (*ptr2)[41];

I = array[4][7]; //等同于 I = *(array[4]+7)  等同于 I = *(*(array +4)+7)

p = array[2];//正确,p指向数组array[2]中下标为0的元素

p = array; // 错误,array是二维数组,其类型为“数组的数组”,类型不匹配

q = array; //错误,array不是指向指针的指针

ptr1 = array; // 正确,同是指向数组int[31]的指针。

ptr2 = array;//错误,array是指向int[31]的指针,ptr2是指向int[41]的指针。

 例二

int array[10];
int(*ptr)[10];
ptr=&array;

/*
上例中ptr是一个指针,它的类型是int(*)[10],他指向的类型是int[10] ,我们用整个数组的首地址来初始化它。在语句ptr=&array中,array代表数组本身。
*/


int(*ptr)[10];
/*
在32位程序中有:
sizeof(int(*)[10])==4
sizeof(int[10])==40
sizeof(ptr)==4

实际上,sizeof(对象)测出的都是对象自身的类型的大小,而不是别的什么类型的大小。
*/

例三

char*str[3]={"Hello,thisisasample!","Hi,goodmorning.","Helloworld"};

chars[80];

strcpy(s,str[0]);//也可写成strcpy(s,*str);

strcpy(s,str[1]);//也可写成strcpy(s,*(str+1));

strcpy(s,str[2]);//也可写成strcpy(s,*(str+2));
/*
上例中,str是一个三单元的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。
把指针数组名str当作一个指针的话,它指向数组的第0号单元,它的类型是char**,指向的类型是char*。
*str也是一个指针,它的类型是char*,它所指向的类型是char,它指向的地址是字符串"Hello,thisisasample!"的第一个字符的地址,即'H'的地址。 
str+1也是一个指针,它指向数组的第1号单元,它的类型是char**,它指向的类型是char*。
*(str+1)也是一个指针,它的类型是char*,它所指向的类型是char,它指向"Hi,goodmorning."的第一个字符'H',等等。
*/

例四:

a.c

/*
如果包含#include "a.h",则编译时报下列错误:
a.c:3:7: error: conflicting types for ‘str1’
 char *str1 = "abcde";
       ^
In file included from a.c:1:0:
a.h:1:13: note: previous declaration of ‘str1’ was here
 extern char str1[];
             ^
a.c:5:6: error: conflicting types for ‘str3’
 char str3[] = "klmno";
      ^
In file included from a.c:1:0:
a.h:3:14: note: previous declaration of ‘str3’ was here
 extern char *str3;

*/
//#include "a.h"

char *str1 = "abcde";
char *str2 = "fghij";
char str3[] = "klmno";
char str4[] = "pqrst";

a.h

extern char str1[];
extern char *str2;
extern char *str3;
extern char str4[];

b.c

#include <stdio.h>

#include "a.h"


void main()
{

    printf("str1:addr=%p, %c, %c, %c, str = %s\n", str1, str1[0], str1[1], str1[2],  str1);
    printf("str2:addr=%p, %c, %c, %c, str_len = %d, str = %s\n", str2, str2[0], str2[1], str2[2], sizeof(str2),  str2);
    printf("str3:addr=%p\n", str3);

    /*如果有下面的代码则运行时报错:Segmentation fault (core dumped)*/
    //printf("str3:addr=%p, %c, %c, %c, str_len = %d str = %s", str3, str3[0], str3[1], str3[2], sizeof(str3),  str3);
    //printf("str3:%c, %c, %c\n", str3[0], str3[1], str3[2]);
    //printf("str3:str_len = %d str = %s\n", sizeof(str3),  str3);
    //printf("str3:str = %s\n",  str3);

    printf("str4:addr=%p, %c, %c, %c, str = %s\n", str4, str4[0], str4[1], str4[2], str4);
    /*如果有下面的代码则编译时报错:error: invalid application of ‘sizeof’ to incomplete type ‘char[]’*/	
    //printf("str1:str_len = %d\n", sizeof(str1));  
    //printf("str4:str_len = %d\n", sizeof(str4)); 
    printf("str1:char[1] addr=%p, char[2] addr=%p\n",&str1[1], &str1[2]);
    printf("str2:char[1] addr=%p, char[2] addr=%p\n",&str2[1], &str2[2]);
    printf("str3:char[1] addr=%p, char[2] addr=%p\n",&str3[1], &str3[2]);
    printf("str4:char[1] addr=%p, char[2] addr=%p\n",&str4[1], &str4[2]);

    return;

}

执行结果如下,可以看到只有声明和定义匹配时输出才正常。

[root@localhost test_extern]# gcc a.c b.c -o test
[root@localhost test_extern]# ./test
str1:addr=0x601038, @, , @, str = @@
str2:addr=0x400746, f, g, h, str_len = 8, str = fghij
str3:addr=0x7170006f6e6d6c6b
str4:addr=0x60104e, p, q, r, str = pqrst
str1:char[1] addr=0x601039, char[2] addr=0x60103a
str2:char[1] addr=0x400747, char[2] addr=0x400748
str3:char[1] addr=0x7170006f6e6d6c6c, char[2] addr=0x7170006f6e6d6c6d
str4:char[1] addr=0x60104f, char[2] addr=0x601050
[root@localhost test_extern]# 

 

 

六、C与C++的混合编程(extern “C”)

extern “C” 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。

被extern "C"限定的函数或变量是extern类型的:

 

实现C++与C及其它语言的混合编程:

被extern"C"修饰的变量和函数是按照C语言方式编译和连接的,未加extern “C”则按照声明时的编译方式。

 

extern "C"的惯用法

(1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为Example.h)时,需进行下列处理:

extern “C”{

#include “Example.h”

}

而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern"C"声明,在.c文件中包含了extern"C"时会出现编译语法错误。

(2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern"C",但是在C语言中不能直接引用声明了extern"C"的该头文件,应该仅将C文件中将C++中定义的extern"C"函数声明为extern类型。

 

七、NULL与NUL

NULL是在<stddef.h>头文件中专门为空指针定义的一个宏。NUL是ASCII字符集中第一个字符的名称,它对应于一个零值。C语言中没有NUL这样的预定义宏。注意:在ASCII字符集中,数字0对应于十进制值48,不要把数字0和'/0'(NUL)的值混同起来。

NULL可以被定义为(void *)0,而NUL可以被定义为'/0'。NULL和NUL都可以被简单地定义为0,这时它们是等价的,可以互换使用,但这是一种不可取的方式。为了使程序读起来更清晰,维护起来更容易,你在程序中应该明确地将NULL定义为指针类型,而将NUL定义为字符类型。

对指针进行解引用操作可以获得它的值。从定义来看,NULL指针并未指向任何东西。因此,对一个NULL指针进行解引用操作是非法的。在对指针进行解引用操作之前,必须确保它并非NULL指针。
 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值