C++ 知识整理 函数

在读《c++primer》的过程中,遇到一些知识点,做一下简单的备忘

c++中函数接受的参数是引用类型的情况下,传递引用的限制非常的严格:

举个例子:

例如,定义函数swap(),实现两个数字值的交换:

#include <iostream>
#include <cstddef>    // inlude ptrdiff_t
#include <vector> 

using namespace std;

void swap(int& a, int& b);   // 定义函数交换两个数的值
 
int main(int argc, char *argv[])
{
    int a1 = 2;
    int a2 = 3;
    cout << "Before swap a=" << a1 << " and b=" << a2 << endl;
    swap(a1, a2);
    cout << "After swap a=" << a1 << " and b=" << a2 << endl;
    
    cout << "-------------------------------" << endl;
    long b1 = 5;
    long b2 = 6;
    cout << "Before swap a=" << b1 << " and b=" << b2 << endl;
    swap(b1, b2);
    cout << "After swap a=" << b1 << " and b=" << b2 << endl;
    
	return 0;
}

void swap(int& a, int& b)
{
	int temp = a;
	a = b;
	b = temp;
} 


运行结果:

C++ 知识整理   函数

书中提到,在第二种情况下,早期c++规则比较宽松,这样的操作不能实现两个数值的交换,因为实参和形参的类型不是严格的匹配,所以编译器将创建两个临时的变量,将long类型的变量转化为int,再赋值给临时变量,所以实际上swap()中进行操作的是临时变量,所以并没有实现变量的值交换。

所以总结是: 如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现,所以应该避免这种临时变量的创建

(但是再我的c++编译器下,还是成功实现了数值交换)

将引用用于结构和类:

为了提高程序的效率,在应用结构和类的时候,传递和返回参数的形式一般会使用引用,而不是按值传递,因为按值传递要栈用额外的空间和时间。再返回引用时需要注意一些问题:

例如:

// 定义一个结构
struct free_throw
{
	string name;
	int age;
	int grade;
}  


const free_throw& copy(free_throw& ft)
{
	free_throw cp;   // cp是一个临时变量 
	cp = ft;
	return cp;       // 返回一个临时变量的引用 
}

上面的函数返回一个临时变量的引用,但是临时变量在函数执行完毕后已经销毁,所以这个引用是不存在的。同理,函数也应该避免返回临时变量的指针。

解决方法:

1. 返回作为参数传递给函数的引用:

const free_throw& copy(free_throw& ft)
{
	free_throw *pt;   // cp是一个指针变量 
	*pt = ft;         // pt指向ft 
	return *pt;       // 返回传入参数的引用 
}

2. 使用new分配新的内存空间,返回内存的地址

但是这种方法隐含了对new()的调用,使得很容易忘记使用delete释放内存

但是可以使用智能指针来解决这种隐含的情况:

函数重载:

函数签名,函数特征标(function signature), 函数参数列表
函数重载中需要注意的条目:

1. 编译器将类型的引用和类型本身视为同一个特征标:

例如:

double cube(double x);
double cube(double& x);   // invalid 

例如上面的情形就是不合法的函数重载

2. 函数匹配时,将不区分const和非const变量

// const与非const的区别
const char p1[20] = "Hello the piano";
char p2[20] = "Hello the macro";
//函数重载
void dribble(char* p); 
void dribble(const char* p);  // 函数重载

void dabble(char* bits);
void drive(const char* p);

dribble(p1);    // 匹配const参数的函数 
dribble(p2);    // 匹配非const参数的函数

// 非const赋给const是合法的
// const赋给非const是不合法的
dabble(p1);   // invalid 参数不匹配
drive(p2);    // 参数匹配 

名称修饰:

c++通过名称修饰来跟踪每一个重载函数。他根据函数型原型中指定的形参类型对每个函数名进行加密。

例如:

long myFunc(int, float)被编译器加密为?myFunc@@YAXH

函数模板:

函数模板是通用的函数描述,它使用泛型来定义函数,其中的泛型可以用具体的类型来替代。由于类型是用参数表示的,因此模板特性也被称为参数化类型。

实际上,模板并不创建函数,而只是告诉编译器如何定义函数。在实际使用时只需要按照实际传入的参数创建函数。

重载的模板:
可以向像重载常规函数那样重载模板定义。

模板的局限性:

某一些类型的操作可能没有定义,例如,对于结构体,没有+-*/的操作。

解决方案:
1. c++允许对一些运算符进行重载,重载运算符即可

2. 为特定类型提供具体化的模板定义

具体化机制介绍:

第三代具体化:(c++98 具体化方法)

1. 对于给定的函数名,可以有非模板函数,模板函数,显示具体化模板函数以及他们的重载

2. 显式具体化的原型和定义以template< >开头,并通过名称来指出类型。

3. 具体化优先于常规模板,非模板函数优先于具体化和常规模板, 即 非模板函数 > 显式具体化 > 模板函数

举一个具体的例子:

// no template function prototype
void swap(job& a, job& b);

// template function prototype
template <typename T>
void swap(T& a, T& b);

// explicit speciallization  显式具体化
template <> void swap<job>(job& a, job& b);

例如,要实现swap函数,对两个结构体进行交换操作:

#include <iostream>

using namespace std;

// template function prototype
template <typename T>
void swap(T& a, T& b);

struct job
{
	char name[40];
	double salary;
	int floor;
};


// explicit speciallization  显式具体化
template <> void swap<job>(job& a, job& b);


int main()
{
	cout.precision(2);   // 设置输出精度
	cout.setf(ios::fixed, ios::floatfield);
	
	int n1 = 10;
	int n2 = 20;
	cout << "n1 =" << n1 << " n2 " << n2 << endl;
	
	swap(n1, n2);
	cout << "n1 =" << n1 << " n2 " << n2 << endl;
	
	p1 = {"zhangsna",
		    45.36,
		    89
			} ;
	
	p2 = {
		"wangwu",
		45.3,
		90
	}
	swap(p1, p2);
	return 0;
}


// function defination
template<typename T>
void swap(T& a, T&b)
{
	T temp;
	temp = a;
	a = b;
	b = temp;     // 结构体允许这样做 
}

template <> void swap<job>(job& a, job& b)
{
	double temp1;
	int temp2;
	
	temp1 = a.salary;
	a.salary = b.salary;
	b.salary = temp1;
	
	temp2 = a.floor;
	a.floor = b.floor;
	b.floor = temp2;
} 

 

函数模板的发展:

模板带来的问题:
1. 在编写模板时,并非总能知道在声明中使用哪种类型:

例如:

template<typename T1, typename T2>
void ft(T1 x, T2 y)
{
	...
	
	?type? sum = x+y;
	
	....
}

那么sum因该是什么类型呢,这个与传入的参数的类型是有关的。因此在c++98中没有办法声明sum的类型。

2.decltype关键字 

c++11通过decltype关键字来提供解决方案

int x;
decltype(x) y;  // 定义y的类型与x相同

所以,上面的函数可以改写为:

template<typename T1, typename T2>
void ft(T1 x, T2 y)
{
	...
	
	decltype(x+y) sum; 
	
    sum = x+y;
	
	....
}

decltype的实际运作原理:

假设有如下声明:

decltype(expression) var;

为了确定类型,编译器会遍历一个核对表:
第一步: 如果expression是一个没有用括号括起来的标识符,和var的类型和标识符的类型相同,包括const等限定符

例如:

double x = 9.0;
double y = 7.9;
double& rx = x;
const double* pd;

decltype(x) w;   // w double
decltype(rx) u = y;   // u double&
decltype(pd) v;   // v double* 

第二步:如果expression是函数调用,则var的类型和函数返回值的类型相同

long indeed(int);
decltype (indeed(3)) y;   // y long

这一操作并不会实际调用函数,编译器只是通过查看函数的原型来获取返回值的类型。

第三步:如果expression为一个左值,则var为其引用类型

例如:

double xx = 4.4;
decltype ((xx)) r2 = xx;  // r2  double&
decltype (xx) w = xx;     // w double

第四步:如果前面的都不满足,则var的类型与expression的类型相同

int j = 4;
int& m = j;
int& n = j;
decltype(j+6) w;   // w  int
decltype(m+n) u;   // u  int; 虽然mn都是引用,但是m+n是两个int的和,所以u的类型为int 

同时,如果需要多次申明,可结合和typedef使用:

template<typename T1, typename T2>
void ft(T1 x, T2 y)
{
	...
	typedef decltype(x+y) xytype;
	
	xytype sum;
	xytype a1; 
	
    sum = x+y;
	
	....
}

decltype无法解决的问题:

具有返回值的模板函数:
例如:

template<typename T1, typename T2>
?type? gt(T1 x, T2 x)
{
	...
	return x+y;
	...
}

无法预先知道参数x,y的类型,所以返回值的类型无法确定,此时还不能使用decltype(x+y)来说明返回值类型,因为此时参数x,y还不再作用域内,必须在声明参数后使用decltype:

C++提供了后置返回类型来解决这种问题:
使用auto:

auto h(int x, float y)->double
{
	...
	//function body;
    ...
}

auto在这里相当于一个占位符,表示后置返回类型提供的类型。

所以上述问题的解决方案可以为:

template<typename T1, typename T2>
auto gt(T1 x, T2 x) -> decltype(x+y)
{
	...
	return x+y;
	...
}

现在参数x,y位于作用域内,所以可以使用它们。

----------------------------------------------------------------------------------------------------------

上一篇:Decltye 随笔


下一篇:Auto 和 Decltye 的区别