6.1 函数基础

  • 函数定义:包括返回类型函数名字0个或者多个形参组成的列表和函数体。
  • 调用运算符:调用运算符的形式是一对圆括号(),作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号内是用逗号隔开的实参列表。

函数的调用完成两项工作:
  1. 用实参初始化函数对应的形参
  2. 将控制权转移给被调用函数。(此时,主调函数的执行被暂时中断,被调函数开始执行)
注:
  • 形参和实参:形参和实参的个数和类型必须匹配上。尽管实参与形参存在对应关系,但是并没有规定实参的求值顺序
  • 返回类型: void表示函数不返回任何值。函数的返回类型不能是 数组类型 或者 函数类型,但可以是指向数组或者函数的指针。

  • 生命周期:对象的生命周期是指 程序执行过程中该对象存在的一段时间。
  • 局部变量:形参和函数体内部定义的变量统称为局部变量。(它对函数而言是局部的,对函数外部而言是隐藏的)
  • 自动对象:只存在于 块执行期间 的对象。当块的执行结束后,它的值就变成未定义的了。
  • 局部静态对象:static类型的局部变量,在 程序的执行路径第一次经过对象定义语句时 初始化,直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
size_t count_calls()
{
    static size_t ctr = 0;     // 调用结束后,这个值仍然有效(str为局部静态对象)
    return ++ctr;
}

int main() {
    for (size_t i = 0; i != 10; ++i)      //(i为局部自动对象)
        cout << count_calls() << endl;

return 0; 
}
注:
  • 局部自动对象未初始化,将产生未定义的值。
  • 局部静态对象未初始化,将默认初始化为0。

  • 函数声明:函数的声明和定义唯一的区别是声明无需函数体,用一个分号替代。函数声明主要用于描述函数的接口,也称函数原型建议函数在头文件中声明,在源文件中定义
  • 分离编译: CC a.cc b.cc直接编译生成可执行文件;CC -c a.cc b.cc编译生成对象代码a.o b.o; CC a.o b.o编译生成可执行文件。

6.2 参数传递

值传递和引用传递:

  • 值传递:指实参的值是通过拷贝传递给形参。(函数对形参做的所有操作 都不会影响实参的值)
  • 引用传递:形参是引用类型,引用形参是它对应的实参的别名;引用形参直接绑定实参对象,而非对象的副本。(对引用形参的操作 可改变实参的值)

使用引用形参可以用于函数返回额外的信息一个函数只能返回一个值, 然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。
// 返回s中c第一次出现的位置索引
// 引用形参occurs负责统计c出现的总次数(虽然occur没有return返回,但是因为是引用类型,所以被引用的实参对象会随occurs改变)
string::size_type find_char(const string& s, char c, string::size_type& occurs)     
{
	auto ret = s.size();      // 第一次出现的位置(如果有的话)
	occurs = 0;               // 设置表示出现次数的形参的值

	for (decltype (ret) i = 0; i != s.size(); ++i) {
		if (s[i] == c) {
			if (ret == s.size()) {
				ret = i;      // 记录c第一次出现的位置
			}
			++occurs;         // 将出现的次数加1
		}
	}
	return ret;      // 出现次数通过occurs隐式地返回
}

建议:
  • 使用引用类型的形参代替指针。
  • 经常用引用形参来避免不必要的复制。
  • 如果无需改变引用形参的值,最好将其声明为常量引用。

const形参和实参:

  • 形参的顶层const被忽略。
  • 我们可以使用非常量初始化一个底层const对象,但是反过来不行。
  • 在函数中,不能改变实参的局部副本。(如果传的是指针,也只是能改指针指向对象,但是指针本身还是改变不了的,除非指针的指针)
  • 不要改变对象的情况下,尽量使用常量引用。(既然不要改,也肯定不想之后被意外改,所以设为const;传引用则可直接对引用所指对象操作,而不需要生成一个对象副本)
void fun1(int i) {}
void fun2(const int i) {}
void fun3(int* i) {}
void fun4(int* const i){}
void fun5(const int* i) {}

int main() {
	int i1 = 1;
	const int i2 = 2;

	fun1(i1);        // 顶层const可以忽略(顶层const只是对象本身,不同于对象的引用或者指针,就算对象相互赋值也不会影响对方对象值)
	fun1(i2);
	fun2(i1);
	fun2(i2);

	fun3(&i1);
	fun3(&i2);      // 实参&i2为 const int*,形参i为 int*(底层const不可忽略。实参i1对象本身不可以改,如果能赋值给i,则i1希望i也不能改i1所指对象,但是这里i是能改的,所以越权了,因此不能传递)
	fun4(&i1);
	fun4(&i2);      // 实参&i2为 const int*,形参i为 int* const(i是个顶层const,&i2是底层const。要是可以,岂不是指针i可以改变常量i2)

	fun5(&i1);      // 形参i为底层const,说明指向常量对象,i1为普通指针当然可以传递(实参i1本身可以改,说明i1允许形参i可以有改或者不改的权力,这里形参i不能改的,所以没有越权,因此可以传递)
	fun5(&i2);      // 实参&i2为 const int*,形参i也是const int *y(形参i没有改所指对象的权力,实参i2有没有,所以没有越权,因此可以传递)
	
}

数组形参:

当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。

(1) 一维数组作为形参时,不需要指定维度的长度(除非是 数组引用形参)
// 尽管形式不同,但这三个print函数是"等价的" (形参都是const int* 类型)
void fun(int*);
void fun(int[]);
void fun(int[5]);      // 5表示我们"期望"数组含有5个元素,实际不一定(5是给人看的,不是必须指定的)

//形参是数组的引用,此时维度是类型的一部分 (编译器不会把数组实参转换为指针,而是传递数组本身,引用形参绑定到实参数组上;  并且编译器检查实参维度的大小与形参维度的大小是否匹配 )
void fun1(int (&a)[5]);       // 维度的长度需指定(数组名可要可不要)

int main() {
    int i = 0;
    int a[2] = { 0,1 };
    int b[5] = { 0,1,2,3,4 };

    fun(&i);	  // 正确: &i的类型是int*
    fun(a);	      // 正确: a转换成int*并指向a[0](这里可看出传递一维数组,并不需数组大小相同)

    fun1(&i);     // 错误:实参不是含有5个整數的数组  
    fun1(a);      // 错误:实参不是含有5个整数的数组 
    fun1(b);      // 正确:实参是含有5个整数的数组
}
(2) 二维数组作为形参时,需要指定第二个维度的长度(第一个不需要)
void fun(int a[][5]);     // 一定要给出第二个维度 (并且编译器检查 实参第二个维度的大小与形参第二个维度的大小 是否匹配)
void fun1(int(*a)[5]);    // "一维数组指针"作为形参

int main() {
	int a[2][3] = { {0,1,2},{3,4,5} };
	int b[2][5] = { {0,1,2,3,4} ,{5,6,7,8,9} };

	fun(a);      // 错误:第二个维度不匹配
	fun(b);      // 正确

	fun1(a);     // 错误:第二个维度不匹配
	fun1(b);     // 正确
}
二维数组与一维数组之间的联系:
  • 传递一维数组可看成传递数组名,即第一个元素指针(类型为 int*)
  • 二维数组是数组的数组,每个元素都是一维数组。传递二维数组也可看成传递二维数组名,即第一个元素(一维数组)的指针,也就是指向一维数组的指针(如 int(*)[5])。

main处理命令行选项:

格式:int main(int argc, char *argv[]){...}
解释:第一个形参代表参数的个数;第二个形参是参数C风格字符串数组。
注:当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;因为argv[0]保存程序的名字,而非用户输入。

含有可变形参的函数:

如果函数的 实参数量未知 但是全部实参的类型相同,可以使用initializer_ list 类型的形参。(C++11)

initializer_list提供的操作:
操作
解释
initializer_listlst;
默认初始化;T类型元素的空列表
initializer_listlst{a,b,c...};
lst的元素数量和初始值一样多;lst的元素是对应初始值的副本;列表中的元素是const
lst2(lst)
拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素
lst2 = lst
同上
lst.size()
列表中的元素数量
lst.begin()
返回指向lst中首元素的指针
lst.end()
返回指向lst中尾元素下一位置的指针

使用示例:
void fun(std::initializer_list<string> il) {     for (auto i = il.begin(); i != il.end(); i++) {         std::cout << *i << std::endl; 
    } 
} 

int main() { 
    std::initializer_list<string> il{ "123", "456" };

    fun(il);                        // 正确 
    fun({ "abc","efd","hij" });     // 正确
    fun({ "abc",1,2 });             // 错误:类型不一致 
}

省略符形参:是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。
省略符形参只能出现在形参列表的最后一个位置,它有两种形式:
void foo(parm_1ist, ...);      // 指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查,
                               // 而省略符形参所对应的实参 无须类型检查。(形参声明后面的逗号是可选的) 
void foo(...);
建议:省略符形参应该仅仅用于C和C++通用的类型。(特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝)

6.3 返回类型和return语句

无返回值函数:

没有返回值的 return语句只能用在返回类型是 void的函数中,返回 void的函数不要求非得有 return语句。

有返回值函数:

  • 主函数main的返回值:如果结尾没有return,编译器将隐式地插入一条返回0的return语句。返回0代表执行成功。
  • 值的返回:返回的值用于 初始化调用点的一个临时量,该临时量就是 函数调用的结果。
  • 引用返回左值:函数的返回类型 决定函数调用是否是左值。调用一个返回引用的函数得到左值;其他返回类型得到右值。
  • 列表初始化返回值:函数可以返回花括号包围的值的列表。(C++11)
vectorfun() {
	return { 1,2,3 };
}
string fun1() {
	return { 'a','b','c' };
}

int main() {
	vectorvec = fun();        // vec={1,2,3}
	string str = fun1();      // str="abc"
}

注:
  • 不要返回局部对象的引用或指针。(当函数结束时局部对象占用的空间也就随之释放掉了,所以return语句指向了不再可用的内存空间
  • return语句的返回值的类型必须和函数的返回类型相同,或者能够隐式地转换成函数的返回类型。
int* fun() {     int a = 1;     return &a;
}

int& fun1() {     int a = 1;     int& t = a;     return t;
}

int fun2() {     double a = 3.14;     return a;
}

int main() {     int* p = fun();      // 错误:*p=1374389535  (函数内局部变量a被释放,p指向一个不可用的空间)     int& b = fun1();     // 错误:b=1374389535   (同理,b引用的对象a被释放掉了)     int c = fun2();      // 正确:c=3 (double可转换为int)     std::cout << *p << " " << b << " " << c; 
}


返回数组指针:

因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。

(1) 使用类型别名(typedef,using)
typedef int arrT[10];     // arrT 是一个类型别名,它表示的类型是含有10个整数的数组
using arrT = int[10];     // arrT 的等价声明

arrT* fun();              // fun()返回一个 指向含有10个整数的数组 的指针

(2) 直接声明(Type ( * function(parameter_list) ) [dimension]
int arr[10];

int(*p)[10] = &arr;     // p是一个指向 含有10个整数的数组 的指针
int(*fun())[10];        // fun()函数返回一个 指向含有10个整数的数组 的指针(对照上面一句比较理解)

(3) 使用尾置返回类型
auto fun()->int(*)[10];      // auto 开头, 形参列表之后是->,紧接着就是函数返回类型(即指向 含有10个整数的数组 的指针)

(4)使用decltype
int arr[10];

decltype(a) *fun();      // 返回一个指向 含有10个整数的数组 的指针(因为是返回的是指针,别忘了(*)!decltype返回一个 含有10个整数的数组类型,所以还要加上(*))

6.4 函数重载

  • 重载:如果同一作用域内几个函数名字相同但形参列表不同,我们称之为重载(overload)函数。(main函数不能重载)
  • 重载和const形参:一个有顶层const的形参和没有顶层const的形参无法区分。相反,是否有某个底层const形参可以区分。
  • 重载和作用域若在内层作用域中声明名字,它将隐藏外层作用域中声明的同名实体,在不同的作用域中无法重载函数名。
//重载
int fun(int a);          
int fun(double a);         // 正确: 参数类型不同
int fun(int a, int b);     // 正确: 参数个数不同
float fun(int a);          // 错误: 和第一个函数相比,只有返回值类型不同,不构成重载

//重载和const形参
int fun(const int a);   
int fun(int a);            // 错误: 和上面函数相比,没有const的形参和有 顶层const 的形参无法区分,不构成重载
int fun(const int* a);
int fun(int* a);           // 正确: 和上面函数相比,没有const的形参和有 底层const 的形参可以区分

//重载和作用域
string read();
void print(const string&);
void print(double);         // 重载print函数

void fooBar(int ival) {
	bool read = false;      // 新作用域:隐藏了外层的read(一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体)
	string s = read();      // 错误: read 是一个布尔值,而非函数

	void print(int);        // 新作用城:隐藏了外层的print(一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体)
	print("Value: ");       // 错误: print(const string &)被隐藏掉了
	print(ival);            // 正确:当前print(int)可见
	print(3.14);            // 正确:但是print(double)被隐藏掉了,调用的是print(int)。(因为double可以隐式转为int类型,所以可以调用)
}
注:在C++语言中,名字查找发生在类型检查之前。

6.5 特殊用途语言类型

默认实参:

默认实参作为形参的初始值出现在形参列表中,我们可以为一个或多个形参定义默认值。(注意:一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值)

使用默认实参调用函数:
void fun(int a = 1, int b = 2, string c = "c") {     std::cout << a << " " << b << " " << c << std::endl; 
} 

int main() { 
    fun();             // 正确: a=1,b=2,c="c"(abc都为默认值) 
    fun(4);            // 正确: a=4,b=2,c="c"(bc为默认值) 
    fun(4, 4);         // 正确: a=4,b=4,c="c"(c为默认值) 
    fun(4, 4, "d");    // 正确: a=4,b=4,c="d" 

    fun("d");          // 错误: 只能省略尾部的实参(实参必须按形参顺序从左到右给,因此可以省略尾部的实参) 
}

默认实参声明:
void fun(int a, int b, int c = 3) {     std::cout << a << " " << b << " " << c << std::endl;
 } 
void fun(int a, int b, int c = 4);        // 错误: 重复定义默认参数(在"给定的作用域"中一个形参只能被赋予一次默认实参。如果fun函数定义到另一个源文件中则可以生效,因为此时没有重复定义)

void fun(int a , int b = 2, int c);       // 正确:添加默认实参(c本来就是默认实参,这是同一个函数,所以没有违反 默认实参都在右边的法则) 
void fun(int a = 4, int b , int c);       // 正确:添加默认实参 

int main() {  
    fun();     // a=4,b=2,c=3
}

默认实参初始化:
int i = 1, j = 2;
double k = 3;

void fun(int a = i, int b = j, int c = k) {     // 注:(1)局部变量不能作为默认实参!(比如fun定义在main后面,前面的声明在main里面,这时绝对不能用main中定义的局部变量来作为默认实参的初始值)     std::cout << a << " " << b << " " << c << std::endl; 
}

int main() { 
    fun();         // a=1,b=2,c=3 

    i = 10;        // 改变默认实参的值(a=10) 
    int j = 10;    // 内层定义的j 隐藏了外层定义的j,但是没有改变外层定义的的j,也就没有改变默认值(这个两个j定义作用域不同,属于俩个不同的变量) 
    fun();         // a=10,b=2,c=3 
}
注:用作默认实参的名字在函数声明所在的作用域内解析,而这些名字的求值过程发生在函数调用时。(所以要注意默认实参的变化,如果改变之后,可能会产生错误的结果)

内联函数和constexpr函数:

内联函数:
  • 内联函数:函数返回类型前 加上关键字inline
  • 普通函数的优点:行为同一,重复利用,易于维护。
  • 普通函数的缺点:调用函数 比求解等价表达式要慢得多。
  • inline函数既有普通函数的优点,又没有函数调用开销的缺点。(可以避免函数调用的开销,可以让编译器在编译时内联地展开该函数)
注:内联说明 只是向编译器发出的一个请求,编译器可以选择忽略这个请求。


constexpr函数:
  • constexpr函数:指能用于常量表达式的函数。(函数的返回类型及所有形参类型 都要是 字面值类型,而且函数体中 必须有且只有一条 retrun语句)
constexpr int fun() {
	return 2;
}
constexpr int m = fun();       // 执行该初始化任务时,编译器把对constexpr函数的调用 替换成其结果值。为了能在编译过程中随时展开,constexpr函数被"隐式地指定为内联函数"。


constexpr int fun1(int a) {    // 如果a是常量表达式则,则fun1(a)也是常量表达式
    return fun() * a;
}   

int main() {
	int arr[fun()];       // 正确:fun()是常量表达式
	
	int i = 1;
	int arr1[fun1(i)];    // 错误:i不是常量表达式,所以fun1(i)返回值 不是常量表达式,(constexpr函数不一定返回常量表达式)
}
注:我们要把内联函数和constexpr函数定义在头文件中。(因为内联函数是内部链接的,如果你在b.cpp中定义这个函数,那么在a.cpp中即使有这个函数声明,但由于内联函数是内部链接的,所以b.cpp不会提供其定义。所以在链接时a.obj无法找到这个函数的定义,便会出现无法解析的外部符号的错误)

constexpr与const的本质区别:
  • const并不能代表“常量”,它仅仅是对变量的一个修饰,告诉编译器这个变量只能被初始化,且不能被”直接修改“。而且这个变量的值,可以在运行时也可以在编译时指定。
  • constexpr可以用来修饰变量、函数、构造函数。一旦以上任何元素被constexpr修饰,那么等于说是告诉编译器 “请大胆地将我看成编译时就能得出常量值的表达式去优化我”。
  • 与const相比,被constexpr修饰的对象则强制要求其 初始化表达式能够在 编译期 完成计算。之后所有引用该常量对象的地方,若非必要,一律用计算出来的常量值替换。

调试帮助:

assert预处理宏:assert(expr);
作用:首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行。如果表达式为真(即非0),assert什么也不做。
开/关 调试状态:CC -D NDEBUG main.c 可以变量NDEBUG。(如果定义NDEBUG,则assert什么也不做)
注:定义NDEBUG能避免检查各种条件所需的运行时开销。(除了用于assert外,也可以使用NDEBUG编写自己的条件调试代码
建议:因此, assert应该仅用于验证那些确实不可能发生的事情。我们可以把assert当成调试程序的一种辅助手段,但是不能用它替代真正的运行时逻辑检查,也不能替代程.序本身应该包含的错误检查。

编译器为每个函数定义了:
  • _ _func_ _ :用于存放函数名字的字符串字面值。
预处理器为程序定了:
  • _ _FILE_ _存放文件名的字符串字面值。
  • _ _LINE_ _ :存放当前行号的整型字面值。
  • _ _TIME_ _ :存放文件编译时间的字符串字面值。
  • _ _DATE_ _ :存放文件编译日期的字符串字面值。
注:这些常量用来 在错误消息中 提供更多信息。

6.6 函数匹配

重载函数匹配的三个步骤: 候选函数 ——> 可行函数 ——>  寻找最佳匹配
  • 候选函数:选定本次调用对应的重载函数集,集合中的函数称为候选函数。(与被调用函数同名,且其声明在调用点可见)
  • 可行函数:考察本次调用提供的实参,选出可以被这组实参调用的函数,新选出的函数称为可行函数。(参数数量相同,参数类型 相同或者可以转换)
  • 寻找最佳匹配:基本思想:实参类型和形参类型越接近,它们匹配地越好。(参数类型相同最好,或者找出匹配结果最好的)

为了确定最佳匹配,编译器将 实参类型到形参类型的转换 划分成几个等级,具体排序如下所示:
  1. 精确匹配:(1)实参类型和形参类型相同。(2)实参从数组类型或函数类型转换成对应的指针类型。(3)向实参添加项层const或者从实参中删除顶层const。
  2. 通过const转换实现的匹配。
  3. 通过类型提升实现的匹配。
  4. 通过算术类型转换指针转换实现的匹配。
  5. 通过类类型转换实现的匹配。

注:如果有且只有一个函数满足下列条件,则匹配成功。(否则编译器会报 二义性调用 的错误提示)
  • 该函数 每个实参的匹配都不劣于 其他可行函数需要的匹配。
  • 至少有一个实参的匹配优于 其他可行函数提供的匹配。
经验:调用重载函数时应 尽量避免强制类型转换。如果在实际应用中 确实需要强制类型转换,则说明我们设计的 形参集合 不合理。

6.7 函数指针

函数指针:是指向函数的指针。
int fun(int a, int b) { return a + b; }
int fun(double a, double b) { return a + b; }
double fun1(double a, double b) { return a + b; }

int main() {
	int (*pf)(int, int);     // pf是一个指向 有两个int参数且返回值为int类型的函数 的指针(注: pf两端括号必不可少,不然pf变成一个返回值为int*类型的函数)

	pf = fun;       // pf指向fun函数(当我们把函数名作为一个值使用时,该函数自动地转换成指针)
	pf = &fun;      // 与上面赋值语句等价: 取地址符是可选的

	int a = pf(1, 1);        // 调用fun函数,a=2(同理,当我们把函数名作为一个值使用时,该函数自动地转换成指针)
	int b = (*pf)(1, 1);     // 等价调用,a=2

	pf = 0;        // 正确,pf不指向任何函数
	pf = fun1;     // 错误: 返回类型不匹配(在指向不同函数类型的指针间不存在转换规则,必须精确匹配!)
	pf = fun;      // 正确: 和fun(int,int)精确匹配(形参类型,形参个数,返回值类型 都要精确匹配)
}
注:
  • 当我们把函数名作为一个值使用时,该函数自动地转换成指针
  • 在指向不同函数类型的指针间不存在转换规则,必须精确匹配!

函数指针形参:形参为函数指针。(虽然不能定义函数类型形参,但是形参可以是指向函数的指针)
int fun(int a, int b);
int fun1(int a, int b, int pf(int, int));       // 第三个形参是函数类型,但它会自动转换成指向函数的指针
int fun1(int a, int b, int(*pf)(int, int));     // 等价声明: 显式地将形参定义成 指向函数的指针


// 使用类型别名或者decltype
typedef int func(int a, int b);
typedef decltype(fun) func1;            // func和fun1是相同的 函数类型

typedef int (*func_p)(int a, int b);
typedef decltype(fun)* func1_p;         // func_p和func1_p是相同的 函数指针类型


int main() {
	fun1(1, 1, fun);     // 自动将函数fun转换成指向该函数的指针(可以直接把函数作为实参使用,此时它会自动转换成指针)

	func f;              // f为 fun函数类型
	func_p f_p;          // f_p为 fun函数指针类型
	fun1(1, 1, f);       // 正确
	fun1(1, 1, f_p);     // 正确
}

返回指向函数的指针:函数返回值为函数类型指针。(虽然不能返回一个函数,但是能返回指向函数类型的指针)
int fun1(int, int) {}
int (*fun(int))(int, int) {  }      // 定义


using func = int(int, int);         // func为 函数类型
using func_p = int(*)(int, int);    // func_p为 函数指针类型


// 使用 类型别名、直接定义、尾置返回类型、decltype 四种方式声明返回函数指针的函数
func_p fun(int);
int (*fun(int))(int, int);
auto fun(int) ->int (*)(int, int);
decltype(fun1)* fun(int);           // (*)别忘记,decltype作用于函数时返回的是函数类型              


func fun(int);      // 错误: func是函数类型,fun不能返回一个函数(必须把返回值写成函数指针形式,编译器不会自动地将 函数返回类型当成对应指针类型处理)
func_p fun(int);    // 正确: func_p是指向函数的指针,fun返回指向函数的指针,并且类型匹配
func* fun(int);     // 正确: 显示地 指定返回类型 是指向函数的指针
注:必须把返回值写成函数指针形式,编译器不会自动地将 函数返回类型当成对应指针类型处理。