c语言面试知识总结

2019-04-14 12:27发布

1、 Const: (1)const修饰的是一个只读变量 (2)节省空间,避免不必要的内存分配,提高效率 编译器通常不为普通const只读变量分配存储空间,而是将它们保存在符号表中,这使 得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。 例如: #define M 3 //宏常量 const int N=5; //此时并未将N放入内存中 ...... int i=N; //此时为N分配内存,以后不再分配! int I=M; //预处理期间进行宏替换,分配内存 int j=N; //没有内存分配 int J=M; //再进行宏替换,又一次分配内存! const定义的只读变量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define 一样给出的是立即数,所以,const定义的只读变量在程序运行过程中只有一份拷贝(因为 它是全局的只读变量,存放在静态区),而#define定义的宏常量在内存中有若干个拷贝。 #define宏是在预处理阶段进行替换,而const修饰的只读变量是在编译的时候确定其值。 #define宏没有类型,而const修饰的只读变量具有特定的类型。 (3)修饰一般变量,数组,指针,函数参数,函数返回值   2、 Static (1)    修饰全局变量,称为静态全局变量。由于全局变量本身存储在静态区,因此本身就是静态的,对全局变量使用静态是告诉编译器这个变量只能在本文件中被使用,不能被extern (2)    修饰局部变量,称为静态局部变量。存储在静态区,即使函数下次调用也不改变其值。 (3)    修饰函数。表示这个函数的作用域仅限于本文件。   3、 如果一个函数没有显式地声明返回值,那返回值就是Int型的 在c语言中,如果一个函数没有显式地说明参数是void,那么是可以使用参数的,如下所示: #include   void test(){          printf("ok "); }   int main(){          //test(3);          return0; } 在c++中不可以
4、 按照ANSI(AmericanNational Standards Institute)标准,不能对void指针进行算法操作, 即下列操作都是不合法的: void * pvoid; pvoid++; //ANSI:错误 pvoid += 1; //ANSI:错误 ANSI标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指 向数据类型大小的。也就是说必须知道内存目的地址的确切值。 例如: int *pint; pint++; //ANSI:正确 但是大名鼎鼎的GNU(GNU's Not Unix的递归缩写)则不这么认定,它指定void *的算法 操作与char *一致。因此下列语句在GNU编译器中皆正确: pvoid++; //GNU:正确 pvoid += 1; //GNU:正确 在实际的程序设计中,为符合ANSI标准,并提高程序的可移植性,我们可以这样编写 实现同样功能的代码: void * pvoid; (char *)pvoid++; //ANSI:正确;GNU:正确 (char *)pvoid += 1; //ANSI:错误;GNU:正确 GNU和ANSI还有一些区别,总体而言,GNU较ANSI更“开放”,提供了对更多语法 的支持。但是我们在真实设计时,还是应该尽可能地符合ANSI标准。 【规则1-36】如果函数的参数可以是任意类型指针,那么应声明其参数为void *。 典型的如内存操作函数memcpy和memset的函数原型分别为: void * memcpy(void *dest, const void *src,size_t len); void * memset ( void * buffer, int c,size_t num ); 这样,任何类型的指针都可以传入memcpy和memset中,这也真实地体现了内存操作 函数的意义,因为它操作的对象仅仅是一片内存,而不论这片内存是什么类型。如果memcpy 和memset的参数类型不是void *,而是char *,那才叫真的奇怪了!这样的memcpy和memset 明显不是一个“纯粹的,脱离低级趣味的”函数! 下面的代码执行正确: 例子:memset接受任意类型指针 int IntArray_a[100]; memset (IntArray_a, 0, 100*sizeof(int) );//将IntArray_a清0 例子:memcpy接受任意类型指针 int destIntArray_a[100],srcintarray_a[100]; //将srcintarray_a拷贝给destIntArray_a memcpy (destIntArray_a, srcintarray_a,100*sizeof(int) ); 有趣的是,memcpy和memset函数返回的也是void *类型,标准库函数的编写者都不是一 般人。     5、 void不能代表一个真实的变量。 因为定义变量时必须分配内存空间,定义void类型变量,编译器到底分配多大的内存呢。 下面代码都企图让void代表一个真实的变量,因此都是错误的代码: void a; //错误 function(void a); //错误 void体现了一种抽象     6、一个定义为volatile变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子: 1. 并行设备的硬件寄存器(如:状态寄存器) 2. 一个中断服务子程序中会访问到的非自动变量(Non-automaticvariables) 3. 多线程应用中被几个任务共享的变量 这是区分C程序员和嵌入式系统程序员的最基本的问题:嵌入式系统程序员经常同硬件、中断、RTOS等等打交道,所有这些都要求使用volatile变量。不懂得volatile内容将会带来灾难。 假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是真正懂得volatile完全的重要性。 1. 一个参数既可以是const还可以是volatile吗?解释为什么。 2. 一个指针可以是volatile 吗?解释为什么。 3. 下面的函数被用来计算某个整数的平方,它能实现预期设计目标吗?如果不能,试回答存在什么问题: 1 2 3 4 intsquare(volatileint*ptr) { return*ptr**ptr; } 下面是答案: 1. 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。 2. 是的。尽管这并不很常见。一个例子是当一个中断服务子程序修改一个指向一个buffer指针时。 3. 这段代码是个恶作剧。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码: 1 2 3 4 5 6 7 intsquare(volatileint*ptr) { inta,b; a=*ptr; b=*ptr; returna*b; } 由于*ptr的值可能在两次取值语句之间发生改变,因此ab可能是不同的。结果,这段代码可能返回的不是你所期望的平方值!正确的代码如下: 1 2 3 4 5 6 longsquare(volatileint*ptr) { inta; a=*ptr; returna*a; }     7、 大小端问题值得注意:跟处理器有关,可以使用程序判定。     8、 enum   在编译阶段确定其值     9、 const修饰的只读变量不能用来作为定义数组的维数, 也不能放在case关键字后面。   1、 Strlen和sizeof的区别 Strlen是一个函数,sizeof是一个运算符。 1、 sizeof(...)是运算符,在头文件中typedefunsigned int,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等。
    
它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。
    
由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小。实际上,用sizeof来返回类型以及静态分配的对象、结构或数组所占的空间,返回值跟对象、结构、数组所存储的内容没有关系。
    
具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:
    
数组——编译时分配的数组空间大小;
    
指针——存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4);
    
类型——该类型所占的空间大小;
    
对象——对象的实际占用空间大小;
    
函数——函数的返回类型所占的空间大小。函数的返回类型不能是void 2、  strlen(...)是函数,要在运行时才能计算。参数必须是字符型指针(char*)。当数组名作为参数传入时,实际上数组就退化成指针了。
    
它的功能是:返回字符串的长度。该字符串可能是自己定义的,也可能是内存中随机的,该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符NULL。返回的长度大小不包括NULL 3、 实际例子: char arr[10] = "What?";
             int len_one = strlen(arr);

             int len_two = sizeof(arr); 
             cout << len_one << " and " <返回值为5 and 10,因为strlen计算的是字符串已经用掉的长度,因此应该为5,而sizeof返回的是获得保证能容纳实现所建立的最大对象的字节大小,这就表示的是这个数组的长度为10 char*t1[20];    char (*t2)[20];    printf("%d%d 0",sizeof(t1),sizeof(t2)); 返回值为80和4。*t1[20]是一个指针数组,本质上是一个数组,他表示一个长为20的数组,数组的每一位是一个指针,因此sizeof(t1)相当于在求一个数组的长度,而这个数组每一位所占的空间是一个指针的大小为4,因此总大小为80;(*t2)[20]是一个数组指针,本质上是一个指针,每个指针下面有20个空间大小,因此sizeof(t2)相当于t2[0]是一个指针,因此所占的大小为4。   2、 如何计算结构体的大小 首先需要明确,在c中,空结构体 structpoint{}; 所占的大小为0,在c++中所占的大小为1。 structpoint{    int num;    char k;    int c; }; structpoint p;    printf("%d ",sizeof(p)); 返回结果为12。这里我们先要明确一点:如何计算结构体的大小。运算符sizeof可以计算出给定类型的大小,对于32位系统来说, sizeof(char) = 1; sizeof(int) = 4 基本数据类型的大小很好计算,我们来看一下如何计算构造数据类型的大小。     C语言中的构造数据类型有三种:数组、结构体和共用体。 数组是相同类型的元素的集合,只要会计算单个元素的大小,整个数组所占空间等于基础元素大小乘上元素的个数。 结构体中的成员可以是不同的数据类型,成员按照定义时的顺序依次存储在连续的内存空间。和数组不一样的是,结构体的大小不是所有成员大小简单的相加,需要考虑到系统在存储结构体变量时的地址对齐问题。看下面这样的一个结构体:        structstu1        {        int    i;        char c;        int j;        } 先介绍一个相关的概念——偏移量。偏移量指的是结构体变量中成员的地址和结构体变量地址的差。结构体大小等于最后一个成员的偏移量加上最后一个成员的大小。显然,结构体变量中第一个成员的地址就是结构体变量的首地址。因此,第一个成员i的偏移量为0。第二个成员c的偏移量是第一个成员的偏移量加上第一个成员的大小(0+4,其值为4;第三个成员j的偏移量是第二个成员的偏移量加上第二个成员的大小(4+1,其值为5 实际上,由于存储变量时地址对齐的要求,编译器在编译程序时会遵循两条原则: 一、结构体变量中成员的偏移量必须是成员大小的整数倍(0被认为是任何数的整数倍) 二、结构体大小必须是所有成员大小的整数倍。 对照第一条,上面的例子中前两个成员的偏移量都满足要求,但第三个成员的偏移量为5,并不是自身(int)大小的整数倍。编译器在处理时会在第二个成员后面补上3个空字节,使得第三个成员的偏移量变成8 对照第二条,结构体大小等于最后一个成员的偏移量加上其大小,上面的例子中计算出来的大小为12,满足要求。 再看一个满足第一条,不满足第二条的情况 struct stu2        {        int   k;        short t; } 成员k的偏移量为0;成员t的偏移量为4,都不需要调整。但计算出来的大小为6,显然不是成员k大小的整数倍。因此,编译器会在成员t后面补上2个字节,使得结构体的大小变成8从而满足第二个要求。 3、 算术运算符 > 关系运算符 > 赋值运算符 因此: X > y+2 也意味着x>(y+2) X=y>2  也意味着 x=(y>2)   4、 在c语言中所有的输入实际上是一个输入流,可以用getchar来接收。 5、 内存分配方式: 内存分配方式有三种:
1从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
2在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。(在函数中不要返回栈内存,但可以返回动态分配的内存)。
3从堆上分配,亦称动态内存分配。程序在运行的时候用malloc new 申请任意多少的内存,程序员自己负责在何时用free delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

例子:
1

voidGetMemory(char*p)
{
     p = (char *)malloc(100);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
}
请问运行Test 函数会有什么样的结果?
答:程序崩溃(段错误)。因为GetMemory并不能传递动态内存,Test函数中的str 一直都是 NULLstrcpy(str,"helloworld");将使程序崩溃。

2
char *GetMemory(void)
{
    char p = "hello world";
    return p;
}
void Test(void)
{
    char *str = NULL;
    str = GetMemory();
    printf(str);
}
请问运行Test 函数会有什么样的结果?
答:可能是乱码。
因为GetMemory返回的是指向栈内存的指针,该指针的地址不是 NULL,但其原现的内容已经被清除,新内容不可知。

3
void GetMemory2(char **p, int num)
{
    *p = (char *)malloc(num);
}
void Test(void)
{
    char *str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}
请问运行Test 函数会有什么样的结果?
答:
1)能够输出hello
2)内存泄漏。

4
void Test(void)
{
    char *str = (char *) malloc(100);
    strcpy(str, “hello”);
    free(str);//
没有将str置为NULL
if(str != NULL)
{
    strcpy(str, “world”);
    printf(str);
}
}
请问运行Test 函数会有什么样的结果?
答:篡改动态内存区的内容,后果难以预料,非常危险。
因为free(str);之后,str 成为野指针,
if(str != NULL)
语句不起作用。

 

6、 数组名实际上是该数组首元素的地址,因此可以这么定义:

Char *p,a[10];       p = a;

但是和数组不同,一个结构的名字不是该结构的地址,因此必须使用&,例如:

Structguy *him, bar;       him = &bar;

  7、 复杂的类型判断中需要记住: 1.    表示一个数组的[ ]和表示一个函数的()具有同样的优先级,这个优先级高于间接运算符 *的优先级。 2.    [ ]()都是从左往右进行结合的。 所以,如下所示: Int * risks[10]: 具有10个元素的数组,每个元素是一个指向Int的指针。 Int (*risks)[10]: 一个指针,指向具有10个元素的数组 Int *oof[3][4]: 一个3*4的数组,每个元素是一个指向int的指针。 Int (*uuf)[3][4]: 一个指针,指向3*4int数组 Int (*uuf[3])[4]: 一个具有3个元素的数组,每个元素是一个指向具有4个元素的int数组的指针 Typedef char(* frptc())[5]: frptc是一个函数,该函数返回一个指向含有5个元素的char数组的指针   8、 函数指针:void (*pf)( ); 指针函数:void *pf(); 使用函数名的所有四种方法: 定义函数;声明函数;调用函数;作为指针       9、 Register关键字  register:这个关键字请求编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率注意是尽可能,不是绝对。你想想,一个CPU 的寄存器也就那么几个或几十个,你要是定义了很多很多register 变量,它累死也可能不能全部把这些变量放入寄存器吧,轮也可能轮不到你。

一、皇帝身边的小太监----寄存器

    不知道什么是寄存器?那见过太监没有?没有?其实我也没有。没见过不要紧,见过就麻烦大了。^_^,大家都看过古装戏,那些皇帝们要阅读奏章的时候,大臣总是先将奏章交给皇帝旁边的小太监,小太监呢再交给皇帝同志处理。这个小太监只是个中转站,并无别的功能。

    
好,那我们再联想到我们的CPUCPU 不就是我们的皇帝同志么?大臣就相当于我们的内存,数据从他这拿出来。那小太监就是我们的寄存器了(这里先不考虑CPU的高速缓存区)。数据从内存里拿出来先放到寄存器,然后CPU 再从寄存器里读取数据来处理,处理完后同样把数据通过寄存器存放到内存里,CPU 不直接和内存打交道。这里要说明的一点是:小太监是主动的从大臣手里接过奏章,然后主动的交给皇帝同志,但寄存器没这么自觉,它从不主动干什么事。一个皇帝可能有好些小太监,那么一个CPU也可以有很多寄存器,不同型号的CPU 拥有寄存器的数量不一样。

    
为啥要这么麻烦啊?速度!就是因为速度。寄存器其实就是一块一块小的存储空间,只不过其存取速度要比内存快得多。进水楼台先得月嘛,它离CPU很近,CPU 一伸手就拿到数据了,比在那么大的一块内存里去寻找某个地址上的数据是不是快多了?那有人问既然它速度那么快,那我们的内存硬盘都改成寄存器得了呗。我要说的是:你真有钱!   二、举例     register修饰符暗示编译程序相应的变量将被频繁地使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存储速度。例如下面的内存块拷贝代码, #ifdefNOSTRUCTASSIGN memcpy(d, s, l) {         register char *d;     register char *s;     register int i;     while (i--)         *d++ = *s++; } #endif
三、使用register 修饰符的注意点     但是使用register修饰符有几点限制。   首先,register变量必须是能被CPU所接受的类型。这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度不过,有些机器的寄存器也能存放浮点数。   其次,因为register变量可能不存放在内存中,所以不能用“&”来获取register变量的地址   由于寄存器的数量有限,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。   在某些情况下,把变量保存在寄存器中反而会降低程序的运行速度。因为被占用的寄存器不能再用于其它目的;或者变量被使用的次数不够多,不足以装入和存储变量所带来的额外开销。   早期的C编译程序不会把变量保存在寄存器中,除非你命令它这样做,这时register修饰符是C语言的一种很有价值的补充。然而,随着编译程序设计技术的进步,在决定那些变量应该被存到寄存器中时,现在的C编译环境能比程序员做出更好的决定。实际上,许多编译程序都会忽略register修饰符,因为尽管它完全合法,但它仅仅是暗示而不是命令   10、          可变宏和可变参数 可变参数函数实现原理: 首先在介绍可变参数表函数的设计之前,我们先来介绍一下最经典的可变参数表printf函数的实现原理。
一、printf函数的实现原理
C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出来,在计算机的内存中,数据有2块,一块是堆,一块是栈(函数参数及局部变量在这里),而栈是从内存的高地址向低地址生长的,控制生长的就是堆栈指针了,最先压入的参数是在最上面,就是说在所有参数的最后面,最后压入的参数在最下面,结构上看起来是第一个,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了,下面给出printf("%d,%d",a,b);(其中ab都是int型的)的汇编代码

[cpp] view plaincopy 1.  .section   2.  .data   3.  string out = "%d,%d"   4.  push b   5.  push a   6.  push $out   7.  call printf   [cpp] view plaincopy 1.  .section   2.  .data   3.  string out = "%d,%d"   4.  push b   5.  push a   6.  push $out   7.  call printf   你会看到,参数是最后的先压入栈中,最先的后压入栈中,参数控制的那个字符串常量是最后被压入的,所以这个常量总是能被找到的。
二、可变参数表函数的设计
     
标准库提供的一些参数的数目可以有变化的函数。例如我们很熟悉的printf,它需要有一个格式串,还应根据需要为它提供任意多个其他参数。这种函数被称作具有变长度参数表的函数,或简称为变参数函数。我们写程序中有时也可能需要定义这种函数。要定义这类函数,就必须使用标准头文件,使用该文件提供的一套机制,并需要按照规定的定义方式工作。本节介绍这个头文件提供的有关功能,它们的意义和使用,并用例子说明这类函数的定义方法。
      C
中变长实参头文件stdarg.h提供了一个数据类型va-list和三个宏(va-startva-argva-end),用它们在被调用函数不知道参数个数和类型时对可变参数表进行测试,从而为访问可变参数提供了方便且有效的方法。va-list是一个char类型的指针,当被调用函数使用一个可变参数时,它声明一个类型为va-list的变量,该变量用来指向va-argva-end所需信息的位置。下面给出va_listC中的源码:

[cpp] view plaincopy 1.  typedef char *  va_list;   [cpp] view plaincopy 1.  typedef char *  va_list;        void va-start(va-list ap,lastfix)是一个宏,它使va-list类型变量ap指向被传递给函数的可变参数表中的第一个参数,在第一次调用va-argva-end之前,必须首先调用该宏。va-start的第二个参数lastfix是传递给被调用函数的最后一个固定参数的标识符。va-start使ap只指向lastfix之外的可变参数表中的第一个参数,很明显它先得到第一个参数内存地址,然后又加上这个参数的内存大小,就是下个参数的内存地址了。下面给出va_startC中的源码: [cpp] view plaincopy 1.  #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )    2.  #define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )   //得到可变参数中第一个参数的首地址   [cpp] view plaincopy 1.  #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )   2.  #define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )   //得到可变参数中第一个参数的首地址         type va-arg(va-list ap,type)也是一个宏,其使用有双重目的,第一个是返回ap所指对象的值,第二个是修改参数指针ap使其增加以指向表中下一个参数。va-arg的第二个参数提供了修改参数指针所必需的信息。在第一次使用va-arg时,它返回可变参数表中的第一个参数,后续的调用都返回表中的下一个参数,下面给出va_argC中的源码: [cpp] view plaincopy 1.  #define va_arg(ap,type)    ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )    //将参数转换成需要的类型,并使ap指向下一个参数   [cpp] view plaincopy 1.  #define va_arg(ap,type)    ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )    //将参数转换成需要的类型,并使ap指向下一个参数        在使用va-arg时,要注意第二个参数所用类型名应与传递到堆栈的参数的字节数对应,以保证能对不同类型的可变参数进行正确地寻址,比如实参依次为char型、char * 型、int型和float型时,在va-arg中它们的类型则应分别为intchar *intdouble.
     void va-end(va-list ap)
也是一个宏,该宏用于被调用函数完成正常返回,功能就是把指针ap赋值为0,使它不指向内存的变量。下面给出va_endC中的源码: [cpp] view plaincopy 1.  #define va_end(ap)      ( ap = (va_list)0 )   [cpp] view plaincopy 1.  #define va_end(ap)      ( ap = (va_list)0 )        va-end必须在va-arg读完所有参数后再调用,否则会产生意想不到的后果。特别地,当可变参数表函数在程序执行过程中不止一次被调用时,在函数体每次处理完可变参数表之后必须调用一次va-end,以保证正确地恢复栈。
   
一个变参数函数至少需要有一个普通参数,其普通参数可以具有任何类型。在函数定义中,这种函数的最后一个普通参数除了一般的用途之外,还有其他特殊用途。下面从一个例子开始说明有关的问题。
假设我们想定义一个函数sum,它可以用任意多个整数类型的表达式作为参数进行调用,希望sum能求出这些参数的和。这时我们应该将sum定义为一个只有一个普通参数,并具有变长度参数表的函数,这个函数的头部应该是(函数原型与此类似):
int sum(int n, ...)

我们实际上要求在函数调用时,从第一个参数n得到被求和的表达式个数,从其余参数得到被求和的表达式。在参数表最后连续写三个圆点符号,说明这个函数具有可变数目的参数。凡参数表具有这种形式(最后写三个圆点),就表示定义的是一个变参数函数。注意,这样的三个圆点只能放在参数表最后,在所有普通参数之后。
下面假设函数sum里所用的