0308
点这里
要使用C++内联函数这项特性,通常的做法是省略原型,用函数定义替代
函数原型,并在函数定义前加上关键字inline
。只有在函数定义的内容很短时,才能用内联方式。
示例:
#include
using std::cout; using std::cin; using std::endl;
inline double square(double x) { return x * x;}
int main(){
double a,b;
cout << "a = " << square(5.0) << endl;
cout << "b = " << square(4.5 + 7.5) << endl;
return 0;
}
C++新增一种复合类型—引用变量。引用是以定义的变量的别名(另一个名称)。
引用变量的主要用途是用作函数的形参:通过将引用变量用作参数,函数将使用原始数据,而不是其副本。
C和C++使用&符号来指示变量的地址;
C++给&符号赋予了另一个含义:将其用作声明引用,例如,int&
指的是 指向int的引用。
看个示例:
#include
using std::cout; using std::cin; using std::endl;
int main(){
int rats = 10;
int& rodents = rats;
cout << "rats = " << rats << "; &rats = " << &rats << endl;//10 addr1
cout << "rodents = " << rodents << "; &rodents = " << &rodents << endl;//10 addr1
rodents++;
cout << "rats = " << rats << "; &rats = " << &rats << endl;//11 addr1
cout << "rodents = " << rodents << "; &rodents = " << &rodents << endl;//11 addr1
return 0;
}
分析:
rodents
的类型时int&
,即指向int变量rats的引用;&rats
和&rodents
中的&
表示地址运算符; int bunnies = 50;
rodents = bunnies;
cout << "rats = " << rats << "; &rats = " << &rats << endl;//50 addr1
cout << "rodents = " << rodents << "; &rodents = " << &rodents << endl;//50 addr1
cout << "bunnies = " << bunnies << "; &bunnies = " << &bunnies << endl;//50 addr2
结果是:
所以说rodents
和rats
是等价的,rodents = bunnies;
就相当于rats = bunnies;
。
rodents
和 rats
的地址一直相同,内容也一直相同。
引用常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名,这种传递参数的方法称为按引用传递。
示例:
#include
using std::cout; using std::cin; using std::endl;
//程序清单8.4:
void swapr(int& a,int& b);
void swapp(int* a,int* b);
void swap(int a,int b);
int main(){
//程序清单8.4:
int m = 20;
int n = 30;
cout << "初始值:m = " << m << "; n = " << n << endl;//20 30
int& a = m;
int& b = n;
cout << "使用引用进行交换:";
swapr(a,b);
cout << "m = " << m << "; n = " << n << endl;//30 20
int* pm = &m;
int* pn = &n;
cout << "使用指针进行交换:";
swapp(pm,pn);
cout << "m = " << m << "; n = " << n << endl;//20 30
cout << "使用值传递进行交换:";
swap(m,n);
cout << "m = " << m << "; n = " << n << endl;//20 30
return 0;
}
void swapr(int& a,int& b){
int temp;
temp = a;
a = b;
b = temp;
}
void swapp(int* a,int* b){
int temp;
temp = (*a);
(*a) = (*b);
(*b) = temp;
}
void swap(int a,int b){
int temp;
temp = a;
a = b;
b = temp;
}
分析:
按引用传递的结果和按地址传递的结果相同,都能完成数据的交换,只有按值传递不能完成数据交换。
先看示例:
#include
using std::cout; using std::cin; using std::endl;
//程序清单8.5:
int cube(int a);
int cube1(int a);
int recube(int& ra);
int recube1(int& ra);
int main(){
//程序清单8.55:
int m = 3;
int n = 5;
cout << "m = " << m << "; n = " << n << endl;
cout << "cube(m) = " << cube(m) << "; m = " << m << endl;
cout << "recube(n) = " << recube(n) << "; n = " << n << endl;
m = 3; n = 5;
cout << "cube1(m) = " << cube1(m) << "; m = " << m << endl;
cout << "recube1(n) = " << recube1(n) << "; n = " << n << endl;
return 0;
}
int cube(int a){
return a * a * a;
}
int cube1(int a){
a = a * a * a;
return a;
}
int recube(int& ra){
return ra * ra * ra;
}
int recube1(int& ra){
ra = ra * ra * ra;
return ra;
}
分析:
cube()和cube1()
的区别在于子函数中有没有对实参的值进行修改,但由于是值传递,所以形参a是实参m的拷贝,所以即使在cube1()中a的值有变化,也不会影响m的值;
recube()和recube1()
的区别也在于子函数中有没有对实参的值进行修改,但由于是引用传递,所以在recube()和recube1()
中,形参ra和实参n是一个东西,所以在recube1()
中,当ra
的值发生变化,main函数中的n
也发生了变化。
结果:
如果不想影响到实参n的变化,可以考虑把引用参数前面加个const
,这样做之后,当编译器发现子函数中修改了ra的值,就会报错,如下:
int recube1(const int& ra){
ra = ra * ra * ra;//这里会报错:ra必须是可修改的左值
return ra;
}
临时变量:
如果引用参数是const
,实参与引用参数不匹配,编译器会在下面两种情况下生成临时变量:
左值:可修改的值,例如普通变量,上面例子中的m
和n
;
非左值:不可修改的值,例如常量(10
)和表达式(m+3
);
修改上面的recube()
:
//const引用参数
int recube(const int& ra){//引用形参是const
return ra * ra * ra;
}
然后看下面的程序:
int side = 26;
int* ps = &side;
int& rs = side;
short age = 5L;
int lens[4] = {1,2,3,4};
recube(side);//形参ra等于side
recube(lens[2]);//形参ra等于lens[2]
recube(rs);//形参ra等于rs,等于side
recube(*ps);//形参ra等于(*ps),等于side
recube(age);//实参short型变量age类型不匹配,但可以转换为int类型,编译器会生成一个临时匿名变量,并让ra指向它,所以ra是临时变量的引用
recube(500);//实参500的类型正确,但不是左值,是个常量,编译器会生成一个临时匿名变量,并让ra指向它,所以ra是临时变量的引用
recube(side + 10);//实参side + 10的类型正确,但不是左值,是个表达式,编译器会生成一个临时匿名变量,并让ra指向它,所以ra是临时变量的引用
上面的倒数三行代码,只有当形参是const时才可行;
没const的话那三行会编译错误:
recube(500);和recube(side + 10);
提示非常量引用的初始值必须为左值;recube(age);
提示无法用short类型的值初始化int&类型的引用(非常量限定)。实际上,对于形参为const引用的C++函数,如果实参不匹配(上面的两种情况),则其行为类似于按值传递,为确保原始数据不被修改,就生成临时变量来临时存储实参值。
结论:应尽可能将引用形参声明为const
。理由有三:
引用非常适合用于结构体和类(用户自定义类型)。
示例:
#include
#include
using std::cout; using std::cin; using std::endl; using std::string;
//程序清单8.6:
struct Students{
string name;
int num;
};
void showStu(const Students& stu);
Students& accumulate(Students& sum_stu,const Students& one_stu);//返回结构体引用
int main(){
//程序清单8.6:
Students stu0 = {"sum"};
Students stu1 = {"reus",26};
Students stu2 = {"messi",50};
Students stu3 = {"ronaldo",60};
Students stu4 = {"marcelo",20};
Students stu5 = {"benzema",10};
Students stu6;
//①
showStu(stu0);//0
//②
accumulate(stu0,stu1);//stu0 + stu1
showStu(stu0);//26
//③
showStu(accumulate(stu0,stu2));//76 stu0 + stu2
//④
accumulate(accumulate(stu0,stu3),stu4);//(stu0 + stu3) + stu4
showStu(stu0);//156
//⑤
accumulate(stu0,stu5) = stu4;//stu4给到stu0
showStu(stu0);//marcelo 20
//⑥
stu6 = accumulate(stu0,stu5);//stu0 + stu5
showStu(stu6);//30
return 0;
}
void showStu(const Students& stu){
cout << "姓名:" << stu.name << ";数量:" << stu.num << endl;
}
Students& accumulate(Students& sum_stu,const Students& one_stu){
sum_stu.num += one_stu.num;
return sum_stu;
}
分析:
1.accumulate()
的返回值是结构体引用,不是结构体;
2.showStu(accumulate(stu0,stu2));//76 stu0 + stu2
是把返回值直接打印出来,返回的是stu0的引用,也就是stu0
本身;
3.accumulate(accumulate(stu0,stu3),stu4);//(stu0 + stu3) + stu4
是先将(stu0
和 stu3
)相加,然后把返回值stu0
的引用(也就是stu0本身)作为外面这层accumulate()
的第一个参数;
4.accumulate(stu0,stu5) = stu4;//stu4给到stu0
虽然是先把(stu0
和 stu5
)相加的结果给到了stu0
,但是返回值stu0
又被赋上stu4
的值,所以accumulate()
实际上没起到作用,就相当于是直接stu0 = stu4;
5.stu6 = accumulate(stu0,stu5);//stu0 + stu5
先把(stu0
和 stu5
)相加的结果给到了stu0
,然后把stu0
赋值给stu6
☆☆☆ 6.如果子函数原型是Students accumulate(Students& sum_stu,const Students& one_stu);//返回值是结构体
那么对于stu6 = accumulate(stu0,stu5);//stu0 + stu5
来说,就是先把(stu0
和 stu5
)相加的结果给到了stu0
,然后把stu0
复制到一个临时位置,再将这个拷贝复制给stu6
,这样的效率就会低,这也就是为什么要返回结构体引用的原因。
☆☆☆☆☆最后是返回引用时需要注意的问题:要避免返回(函数终止时不再存在的内存单元的)引用,即(在子函数中声明的局部变量的)引用,类似这样的代码:
const Students& clone1(Students stu){
Students temp
temp = stu;
return temp;
}
这个函数返回的是指向临时变量temp的引用,函数执行完temp将不再存在!即返回的内存空间根本就不存在。
为避免这种问题,最简单的方法是,返回一个(作为参数)传递给函数的引用
,即返回形参列表中的一个引用,例如上面的示例程序中返回形参stu0的引用。
和8.2.4类似,也是强调了返回引用时需要注意的问题:要避免返回(函数终止时不再存在的内存单元的)引用,即(在子函数中声明的局部变量的)引用
。
能够将一个特性从一个类传递给另一个类的语言特性被称为继承。
例如,ostream
类是基类,而ofstream
类是派生类,派生类继承了基类的方法,就意味着ofstream
对象可以使用基类的特性。
同理,基类引用可以指向派生类对象,而无需进行强制类型转换。因此,可以定义一个(将基类引用作为参数的)函数,调用该函数时,可以将基类对象作为参数,也可将派生类对象作为参数。
例如,参数类型为ostream&
的函数可以接受ostream
类对象(例如cout
)作为参数,也可以接受(用户自己生命的ofstream
类对象ofs
)作为参数。
因此就可以写这样一个函数:
给形参传递cout
时就是将内容显示到屏幕上;
给形参传递ofs
时就是将内容写到文件中。
看示例:
#include
#include
#include
#include
using std::cout; using std::cin; using std::endl; using std::string; using std::ostream;
using std::ofstream; using std::ios_base;using std::ios;
//程序清单8.8:
void printOrWrite(ostream& os, const string& name,const int arr[]);
int main(){
//程序清单8.8:
string name = "my name is Reus.";
int arr[5] = {1,2,3,4,5};
// cout << name << endl;
// int i;
// for(i = 0;i < 4;i++)
// cout << arr[i] << ", ";
// cout << arr[i] << endl;
//写到文件中:
string fileName = "test.txt";
ofstream ofs;
ofs.open(fileName);
if(!ofs.is_open()){
cout << "文件打开错误!" << endl;
return -1;
}
printOrWrite(ofs,name,arr);
//打印:
printOrWrite(cout,name,arr);
return 0;
}
void printOrWrite(ostream& os, const string& name,const int arr[]){
os << name << endl;
int i;
for(i = 0;i < 4;i++)
os << arr[i] << ", ";
os << arr[i] << endl;
}
分析:
1.刚开始一直编译不成功,找了很久,后来发现是printOrWrite()
函数原型中string
前面少写了个const
;
2.单独写一个函数,这样打印和写入到文件就可以直接调用,会很方便,这就是一个派生类继承基类方法的案例;
3.引用比值传递效率要更高,但要警惕是否会改动到原始数据。
使用引用参数的主要原因有两个:
那么,什么时候应使用引用、什么时候应使用指针呢?什么时候应按值传递呢?
下面是一些指导原则:
数据对象 | 使用传递的值但不做修改的函数 | 要修改调用函数中数据的函数 |
---|---|---|
内置数据类型或小型结构体 | 按值传递 | 使用指针 |
数组 | const指针 (唯一的选择) |
使用指针(唯一的选择) |
大型结构体 | const指针 或 const引用 |
引用或指针 |
类对象 | const引用 |
引用 |
备注:传递类对象参数的标准方式是按引用传递。 |
当然,这只是一些指导原则,很可能有充分的理由做出其他的选择。
例如,对于基本类型,cin
使用引用,因此可以使用 cin >> n
,而不是cin >> &n
(P220 8.2.5上面的一行)。
1.设置默认值,必须通过函数原型
:
void func1(string name,int m = 1);//函数原型
在main函数中调用func1
时,
如果没传入第二个参数,那就相当于是使用了默认值,例如:func1("reus");
就相当于func1("reus",1);
如果传入了第二个参数,那么默认值将被覆盖,例如:func1("reus",3);
中3将覆盖默认值
2.要为某个参数设置默认值,必须为其右边所有的参数提供默认值;
int func2(int m,int n = 4,int l = 5);//ok
int func3(int m,int n = 4,int l);//不ok
int func4(int m = 3,int n = 4,int l = 5);//ok
3.只需在函数原型中指定默认值,函数定义中不需要再次指定默认值;
看个示例:
#include
#include
using std::cout; using std::cin; using std::endl; using std::string;
//程序清单8.9:
void func1(string name,int m = 1);//函数原型:设置了默认值
int main(){
//程序清单8.9:
func1("reus");
func1("messi",3);
return 0;
}
void func1(string name,int m){//函数定义中不需要再次指定默认值
cout << "name = " << name << "; m = " << m << endl;
}
概念:
函数重载即函数多态,能够让用户使用多个同名的函数,这两个术语指的是一回事,但我们通常使用函数重载。
要点1234:
1.函数重载的关键在于函数的参数列表,也称为函数特征标(function signature)。首先是两个函数的函数名相同,其次是两个函数的参数个数、参数类型、参数的排列顺序不同,则可以构成重载,例如:
void func(int n,float m);
void func(int n,float m,float x);//参数个数不同
void func(int n,int m);//参数类型不同
void func(float m,int n);//参数的顺序不同
2.变量名和返回值类型就无关紧要了,不是构成重载的必要条件,例如:
long func1(int n,float m);
//double func1(int n,float m);//不ok
//long func1(int x,float y);//不ok
参数的个数、类型、顺序都相同,仅仅是返回值类型和变量名不一样,所以不能构成重载。无论返回值类型和变量名是否相同,都与构成重载无关。
3.另外,某种数据类型引用和这种数据类型本身是同一个东西,所以不能构成重载,例如:
double cube(double x);
//double cube(double& x);//不能构成重载
引用和自身是一回事
,因此二者不能共存。
4.在匹配参数时,要注意区分const变量
和非const变量
,例如:
void func2(char* name);
void func2(const char* name);
void func3(char* name);
参数类型相同,所以构成重载,但在调用func2的时候要注意匹配参数,
char* name1 = "123asdfg";
const char* name2 = "456asdfqwe";
func2(name1);//func2(char* name);
func2(name2);//func2(const char* name);
func3(name1);//func3(char* name);
//func3(name2);//错误,没有与之匹配的函数
函数func3
不能接受name2
,因为func3
只能接受非const的变量
,而name2
是const变量
。所以说:
非const参数
只与 非const变量
匹配;const参数
与 非const变量、const变量、右值参数(表达式)
都匹配。代码:
#include
#include
#include
using std::cout; using std::cin; using std::endl; using std::string;
//程序清单8.10:
unsigned long func1(const long& num,int n);//返回数字num的前n位数字
char* func1(const char* str,int n);//返回字符串str的前n个字符
int main(){
//程序清单8.10:
cout << "程序清单8.10的结果:" << endl;
unsigned long num = 12345;
const char* str = "asdfg";
char* res;
for(int i = 0;i < 7;i++){
cout << func1(num,i) << endl;
res = func1(str,i);
cout << res << endl;
delete[] res;
}
cout << "Done!" << endl;
return 0;
}
unsigned long func1(const long& num,int n){
int weishu = 0;
unsigned long subNum = 0;
unsigned long temp = num;
while(temp > 0){
weishu++;
temp /= 10;
}
//cout << num << "的位数是:" << weishu << endl;
if(num == 0 || n == 0){
return 0;
}
if(weishu < n)
return num;//返回原数字
subNum = num;
for(int i = 0;i < (weishu - n);i++){//要显示前n位,那就进行(weishu - n)次/10操作
subNum = subNum / 10;
}
//cout << num << "的前" << n << "位数是:" ;
return subNum;
}
char* func1(const char* str,int n){
char* res = new char[n + 1];
int i;
for(i = 0;i < n && str[i] != '\0';i++){
res[i] = str[i];
}
while(i <= n)
res[i++] = '\0';
return res;
}
仅当函数基本上执行相同的任务,但使用不同形式的数据(不同的数据类型、个数、顺序
)时,才应采用函数重载。
函数模板是通用的函数描述,也就是说他们使用 泛型 来定义函数,其中的泛型可用具体的类型(int
或double
)替换。通过将类型作为参数传递给模板,可是编译器生成该类型的函数。
例如,想要交换各种数据类型(int型、float型、double型)的两个数据,一种方法是利用上一节的函数重载,如下:
void swap(int& a,int& b);
void swap(float& a,float& b);
void swap(double& a,double& b);
但是这样就需要写多个函数原型和函数定义,既浪费时间,又容易出错。
因此,可以考虑建立一个交换两个数的模板
:
template <typename T>
//template
void swap(T& a, T& b);//函数原型
其中template
和 typename
是必需的,也可以用class
替换typename
;
函数原型上面的代码表示:建立一个模板,并将类型命名为T
。这个T
可以用任何具体的类型(如int型、float型、double型
)替换。
注意:模板并不创建任何函数,而是告诉编译器如何定义函数。
模板函数的定义
:
//template
template <class T>
void swap(T& a, T& b){
T temp;
temp = a;
a = b;
b = temp;
}
示例:
#include
#include
#include
using std::cout; using std::cin; using std::endl; using std::string;
//建立一个模板函数:
template <typename T>
//template
void swap(T& a, T& b);
int main(){
int a,b;
a = 30; b = 50;
cout << "交换前,a = " << a << ";b = " << b << endl;
swap(a,b);
cout << "交换后,a = " << a << ";b = " << b << endl;
double x,y;
x = 30.6; y = 50.9;
cout << "交换前,x = " << x << ";y = " << y << endl;
swap(x,y);
cout << "交换后,x = " << x << ";y = " << y << endl;
return 0;
}
//模板函数的定义:
//template
template <class T>
void swap(T& a, T& b){
T temp;
temp = a;
a = b;
b = temp;
}
分析:
void swap(int& a, int& b)
和void swap(double& a, double& b)
。使用模板的好处是,它使得生成多个函数定义的行为更简单、更可靠。函数模板也有重载的情况(函数名相同,但参数个数和参数类型不同),例如:
//交换两个数的模板:
template <typename T>
void swap(T& a, T& b);
//交换两个数组的模板:
template <typename T>
void swap(T a[], T b[],int n);
//void swap(T* a, T* b,int n);
示例:
#include
using std::cout; using std::cin; using std::endl; using std::string;
//交换两个数的模板:
template <typename T>
//template
void swap(T& a, T& b);//原型
//交换两个数组的模板:
template <typename T>
void swap(T a[], T b[],int n);//原型
//void swap(T* a, T* b,int n);
template <typename T>
void showArr(T arr[],int n);
int main(){
//程序清单8.12:
int arr1[5] = {1,2,3,4,5};
int arr2[5] = {6,7,8,9,0};
cout << "修改前:" << endl;
showArr(arr1,5);
showArr(arr2,5);
swap(arr1,arr2,5);
cout << "修改后:" << endl;
showArr(arr1,5);
showArr(arr2,5);
double arr3[5] = {1.1,2.2,3.3,4.4,5.5};
double arr4[5] = {6.6,7.7,8.8,9.9,0.0};
cout << "修改前:" << endl;
showArr(arr3,5);
showArr(arr4,5);
swap(arr3,arr4,5);
cout << "修改后:" << endl;
showArr(arr3,5);
showArr(arr4,5);
return 0;
}
//模板函数的定义:
//template
template <class T>
void swap(T& a, T& b){
T temp;
temp = a;
a = b;
b = temp;
}
template <typename T>
void swap(T a[], T b[],int n){
T temp[n];
for(int i = 0;i < n;i++){
temp[i] = a[i];
a[i] = b[i];
b[i] = temp[i];
}
}
template <typename T>
void showArr(T arr[],int n){
int i;
for(i = 0;i < n-1;i++){
cout << arr[i] << ",";
}
cout << arr[i] << "." << endl;
}
template <class T>
void swap(T& a, T& b);
template <class T>
void swap(T& a, T& b){
b = a;
if(a > b){}
T c = a * b;
}
分析:
1.如果T
是数组,那么b = a;
就不成立,因为这样只会把数组a的地址给到b,不能实现整体的赋值;
2.如果T
是结构体,那么 if(a > b){}
就不成立,因为结构体没办法比较大小;
3.如果T
数组、指针或者结构体,那么T c = a * b;
就不成立。
一种方法是考虑重载运算符;还有一种方法是为特定类型提供具体化
的模板定义,见下一节。
//结构体:
struct jober
{
string name;
int age;
double salary;
};
//非模板函数:
void swap(job &, job &);
//模板函数:(常规模板函数)
template <typename T>
void swap(T &, T &);
//显式具体化模板函数:
template<> void swap<job>(job &, job &);
结论:
优先级:非模板函数
优于 显式具体化模板
优于 常规模板
。
先要有一个常规模板
,才能创建各个具体类型的具体化模板
。
//常规模板:
template<class T>
void Swap(T &, T &);
//int类型的具体化模板:
template <> void Swap<int>(int& ,int&);//ok
template <> void Swap(int& ,int&);//也ok,可以省略第二个<>
//结构体类型的具体化模板:
struct jober
{
string name;
int age;
double salary;
};
template<> void swap<jober>(jober& a,jober& b);//ok
template<> void swap(jober& a,jober& b);//也ok,可以省略第二个<>
常规模板
和具体化模板
的例子:(具体化模板 优于 常规模板)#include
#include
#include
using std::cout; using std::cin; using std::endl; using std::string;
//程序清单8.13:
//常规模板函数:
template <typename T>
void swap(T& a,T& b);//常规模板的原型声明
struct jober
{
string name;
int age;
double salary;
};
//具体化模板函数:
template<> void swap<jober>(jober& a,jober& b);//具体化模板的声明
void showStruct(const jober& j);//非模板函数的声明
int main(){
int a,b;
a = 20; b = 30;
cout << "交换前:a = " << a << ";b = " << b << endl;
swap(a,b);
cout << "交换后:a = " << a << ";b = " << b << endl;
jober j1 = {"reus",26,50.5};
jober j2 = {"messi",28,30.6};
cout << "交换前:" << endl;
showStruct(j1);
showStruct(j2);
swap(j1,j2);
cout << "交换后:" << endl;
showStruct(j1);
showStruct(j2);
return 0;
}
template <typename T>
void swap(T& a,T& b){//模板函数的实现
T temp;
temp = a;
a = b;
b = temp;
}
template<> void swap<jober>(jober& a,jober& b){//模板函数具体化的实现
string tempName;
int tempAge;
double tempSalary;
tempName = a.name;
a.name = b.name;
b.name = tempName;
tempAge = a.age;
a.age = b.age;
b.age = tempAge;
tempSalary = a.salary;
a.salary = b.salary;
b.salary = tempSalary;
}
void showStruct(const jober& j){//非模板函数的实现
cout << "姓名:" << j.name << "; 年龄:" << j.age << ";薪资:" << j.salary << endl;
}
请记住,模板
并不等同函数定义
,在包含函数模板的代码中也并不会自动生成函数定义,模板只是提供了一个生成函数定义的方案。编译器使用模板为特定类型生成函数定义,得到一个此类型的模板实例
(instantiation),这就是一个实例化
的过程。
例如在上一节程序清单8.13中,main函数调用swap(a,b);
之后编译器会生成swap()模板的一个实例,该实例使用的是int
类型,因此说该实例是一个int
类型的函数定义,这就是一个实例化
的过程,并且这种实例化方式被称为隐式实例化
(implicit instantiation)。
此外,C++还允许显式实例化
(explicit instantiation),这种实例化方式可以直接命令编译器创建特定的实例,如下:
template void Swap<int>(int&, int&);
编译器看到上述声明后,就会使用swap()模板生成一个int类型的模板实例,即上述声明的意思是“使用swap()模板生成int类型的函数定义”。
综上,
1.模板实例类似于函数定义,模板类似于函数声明;
2.利用模板生成模板实例的过程即实例化。
二者的区别在于:
隐式实例化是在函数调用swap(a,b)
时,编译器使用swap()模板生成一个int类型的模板实例;
而显式实例化是直接通过声明的方式
来直接命令编译器使用swap()模板生成一个int类型的模板实例。
使用模板生成一个具体类型
的模板实例,也即使用模板生成具体类型
的函数定义:
使用模板生成一个int类型
的模板实例,也即使用模板生成int类型
的函数定义;
使用模板生成一个double类型
的模板实例,也即使用模板生成double类型
的函数定义;
使用模板生成一个结构体类型
的模板实例,也即使用模板生成结构体类型
的函数定义;
//显式实例化:
template void Swap<int>(int&, int&);
//显式具体化:
//int类型的具体化模板:
template <> void Swap<int>(int& ,int&);//ok
template <> void Swap(int& ,int&);//也ok,可以省略第二个<>
显式具体化的意思是“不要
使用swap()模板来生成函数定义(模板实例),而应使用
(专门为int类型显式定义的)函数定义”,即这个原型有且必须要有自己的函数定义
。
注意:
1.在一个文件中同时使用同一种类型的显式示例
和显式具体化
将会报错!!!
2.二者的区别在于显式具体化的声明使用前缀template <>
,而显式实例化的声明使用template
,没有<>
。
1.隐式实例化、显式实例化和显式具体化统称为具体化specialization。他们的相同之处在于,他们表示的都是使用具体类型的函数定义,而不是通用描述。
2.
调用常规模板
生成具体类型的函数定义(模板实例)的过程是隐式实例化
;
调用显式具体化模板
生成具体类型的函数定义(模板实例)的过程是显式具体化
;前缀template<>
调用显式实例化模板
生成具体类型的函数定义(模板实例)的过程是显式实例化
;前缀template
3.同一数据类型的显式具体化
和显式实例化
不能共存;
4.显式具体化模板
在main函数外面,显式实例化模板
在main函数之内;
5.实例化是个过程,具体化是为某个具体类型提供(单独的属于自己的)函数定义;
6.具体化模板
的实现,是在常规化模板
的基础上实现的,即具体化模板不能单独存在。
非模板函数
优于 具体化模板
优于 常规模板
先看有没有非模板函数,有就直接调,
没有的话再看具体化模板,有调用具体化模板,
没有的话再看有没有常规模板,有的话就调用常规模板,没有的话就会报错,显示该函数调用出错。
(更具体的看8.5.5 编译器选择使用哪个函数版本)
...
//常规模板:
template <class T>
void Swap(T &, T &);
//(显式)具体化模板:
template<> void Swap<job>(job &, job &);
int main(){
//显式实例化模板:
template void Swap<char>(char &, char &);//编译器使用模板来生成Swap()的char版本的模板实例
int a,b;
...
Swap(a,b);//int类型的隐式实例化,调用最上面的常规模板
job m,n;
...
Swap(m,n);//job类型的显式具体化,调用main函数上面声明的job类型的具体化模板,将使用为job类型提供的独立定义
char x,y;
...
Swap(x,y);//char类型的显式实例化,调用main函数下面声明的char类型的显式实例化模板
...
}
提升转换:char 和 short 自动转换为 int ;float 自动转换为 double;
标准转换:int 转换为char ;long 转换为 double;
简而言之,重载解析将寻找最匹配的函数:
如果只存在一个这样的函数,则选择它;
如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数;非模板优于模板
如果存在多个适合的函数,且它们都为模板函数,但其中有一个函数比其他函数更具体,则选择该函数;具体化模板优于常规模板
如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用将是不确定的,因此是错误的。如果不存在匹配的函数,则也是错误。更具体是指编译器推断使用哪种类型时执行的转换最少
decltype
假如有以下模板:
template <class T1, class T2>
void ft(T1 x, T2 y){
...
?type? xpy = x + y;
...
}
考虑xpy是什么类型?
可能是T1、T2或者其他类型。
当T1是double型,T2是int型,x+y是double型,xpy是T1类型;
当T1是short型,T2是int型,x+y是int型,xpy是T2类型;
当T1时short型,T2是char型,x+y的结果自动整型提升,xpy是int型。
C++新增的关键字decltype
提供了解决方案:
template <class T1, class T2>
void ft(T1 x, T2 y){
...
decltype(x + y) xpy = x + y;
...
}
当使用关键字decltype
时,为确定类型,编译器必须遍历一个核对表。假如有如下声明:
decltype(expression) var;
则核对表的简化版如下:
1.如果expression是一个没有用括号括起的标识符,则var的类型与该标识符的类型相同,包括const等限定符:
double x = 5.5;
double y = 7.9;
double& rx = x;
const double* pd;
decltype(x) w; //w和x的类型相同,double
decltype(rx) u = y; //u和rx的类型相同,double&
decltype(pd) v; //v和pd的类型相同,const double*
2.如果expression是一个函数调用,则var的类型与函数的返回类型相同:
long indeed(int);
decltype(indeed(3)) m;//m和indeed()函数的返回值类型相同,即long
3.如果expression是一个用括号括起来的标识符,则var为指向其类型的引用:
double xx = 4.4;
decltype ((xx)) r2 = xx;//r2是指向括号里的xx的引用,即double&
decltype (xx) w = xx;//w和xx的类型相同,即double
4.如果前面的条件都不满足,则var的类型与expression的类型相同:
int j = 3;
int& k = j
int& n = j;
decltype(j + 6) i1;//i1是int
decltype(100L) i2; //i2是long
decltype(k + n) i3;//i3是int
请注意,虽然k和n都是引用,但表达式k + n
不是应用,他是两个int的和,所以i3
是int
类型。
8.1 内联函数
8.2 引用
8.2.6 继承
8.3 默认参数,即参数的默认值
8.4 函数重载(也叫函数多态)
8.5 函数模板(Template)—泛型编程
通常情况下,程序设计中代码较简单的非递归函数可以使用内联函数。
设置默认参数后,
函数默认值只需要在声明处
表示,在函数的定义处
不需要标注参数默认值。
double mass(double density, double volume = 1.0);
函数重载方式如下:
double mass(double density);
double mass(double density, double volume);
b.由于函数必须从右向左提供默认值,因此无法使用默认值,只能使用函数重载实现:
void repeat (int n,char* str);
void repeat (char* str);
c.题目中两个函数的参数类型不同,可以通过函数重载实现:
int average(int a,int b);
double average(double a,double b);
d.题目中函数的特征标相同,因此无法实现函数的重载。另外,由于返回的(字符’I’和指向字符串"I am glad to meet you"的)指针都是char*
类型,因此返回值也相同。
题目b的解读:
第一句:将指定的字符串显示指定次;
第二句:将指定的字符串显示默认次,也就是5次。
我尝试给两个参数都设置一个默认值:
void repeat(int n = 5 ,const char* str = "123");
但是在调用的时候,
要么就两个参数都传入,这样可以实现第一句,即将指定的字符串显示指定的次数;
要么一个参数都不传入,直接使用两个默认参数,但这样就没办法实现第二句,即将指定的字符串显示默认(5)次。
也就是说,不能只指定第二个参数,而使用第一个参数的默认值,因为函数必须从右向左提供默认值,换言之,如果要使用第一个默认参数,那它后面的所有参数都要使用默认值。
//第6题:
repeat();
repeat(3,"abc");
//repeat("messi");//错误!不能只输入一个字符串而不输入次数
因此,题目b不能使用默认参数的方式实现,只能使用函数重载的方法实现。
因此,下面代码中的常规化模板
template<class T>
T& bigger(T& b1, T& b2);//模板函数的原型声明
是必不可少的,否则具体化模板
template<> box& bigger<box>(box& b1,box& b2);//模板的具体化声明
根本无法编译通过。
代码:
#include
using std::cout; using std::cin; using std::endl; using std::string;
//第8题:显式具体化要在原有模板函数的基础上,具体化特定类型的实现。
template<class T>
T& bigger(T& b1, T& b2);//模板函数的原型声明
struct box
{
char maker[40];
float height;
float width;
float length;
};
template<> box& bigger<box>(box& b1,box& b2);//模板的具体化声明
void showStruct(const box& b);
int main(){
//第8题:
box b1 = {"China", 20.2, 30.3, 1.1};
box b2 = {"indian", 2.2, 3.3, 0.1};
showStruct(b1);
showStruct(b2);
cout << "比较大的box的信息为: " << endl;;
box temp = bigger(b1,b2);
showStruct(temp);
return 0;
}
void showStruct(const box& b){
cout << "maker:" << b.maker << "; height = " << b.height << "; width = " << b.width << ";length = " << b.length << endl;
}
template<class T>
T& bigger(T& b1, T& b2){//模板函数的实现
return b1 > b2 ? b1 : b2;
}
template<> box& bigger<box>(box& b1,box& b2){//模板函数具体化的实现
if(b1.length > b2.length)
return b1;
else
return b2;
}
题目分析:
1.子函数的第二个参数设置为默认参数,默认值为0,当传入第二个(非0)参数的时候,默认值会被覆盖;
2.在子函数中设定一个静态值
,用来存储函数被调用的次数static int count = 0;
函数内的静态变量能够在函数结束后仍保留在程序存储区内,并在一下次调用函数时,再一次被函数继续使用。
3.子函数中判断第二个参数是否为0,为0就打印一次,不为0就循环打印count
次。
代码:
#include
#include
using std::cout; using std::cin; using std::endl; using std::string;
//第1题:
void print(const char* str,int n = 0);
int main(){
//第1题:
print("123");
print("456",10);//n != 0,此时print函数被调用了2次
print("789");
print("abc",1);//n != 0,此时print函数被调用了4次
print("xyz");
print("zyx",3);//n != 0,此时print函数被调用了6次
return 0;
}
void print(const char* str,int n){
static int count = 0;
count++;
if(n == 0){
cout << "n == 0,打印 1 次:\n" << str << endl;
}
else{
cout << "n != 0,打印 " << count << " 次:" << endl;
for(int i = 0;i < count;i++){
cout << str << " / ";
}
cout << endl;
}
}
#include
using std::cout; using std::cin; using std::endl;
int func1();//返回函数被调用的次数
int main(){
func1();
func1();
func1();
func1();
func1();
func1();
func1();
cout << "函数func1被调用的次数为:" << func1() << endl;
return 0;
}
int func1(){
static int count = 0;
count++;
return count;
}
结果:
函数func1被调用的次数为:8
代码:
#include
#include
using std::cout; using std::cin; using std::endl; using std::string;
//第2题:
struct CandyBar
{
char brandName[30];//1.这里要写字符数组,不能写char* brandName;
double weight;
int calorie;
};
void setCandyBar(CandyBar& c,const char* name = "Munch Box",const double weight = 2.85,const int calorie = 350);//2.这里要写const char* name = "Munch Box",不能写char* name = "Munch Box"
void showStrc(const CandyBar& c);
int main(){
//第2题:
CandyBar c1;
CandyBar c2;
setCandyBar(c1);
showStrc(c1);
setCandyBar(c2,"wangzai",3.6,270);
showStrc(c2);
return 0;
}
void setCandyBar(CandyBar& c,const char* name,const double weight,const int calorie){
strcpy(c.brandName,name);//3.学习strcpy()函数的原型和使用方法
c.weight = weight;
c.calorie = calorie;
}
void showStrc(const CandyBar& c){
cout << "品牌名:" << c.brandName << "; 重量:" << c.weight << "; 热量:" << c.calorie << endl;
}
分析:
程序中的123点都涉及到strcpy()
函数的使用,具体分析见下面的char* 和 const char* 和 字符数组 和 strcpy()
函数。
strcpy()
函数参考链接:strcpy()函数用法及其详解
strcpy()
函数声明:
char *strcpy(char *dest, const char *src)
函数参数:
dest
– 指向用于存储复制内容的目标数组,char*
要指向一个数组,不能指向一个char*
,这个参数也不能是const char*
类型。src
– 要复制的字符串,只能说const char*
类型,不能是char*
类型。函数功能:
把 `src` 所指向的字符串复制到 `dest`。
返回值:
该函数返回一个指向最终的目标字符串 dest 的指针。
参数分析:
1.第一个参数dest
是个char*
类型,它指向一个字符数组
,并且一个字符数组的位置和所占内存大小都是确定的。下面这样的写法是错误的
:
char *str;
strcpy(str,"The C of Tranquility");
strcpy()
把"The C of Tranquility"
拷贝到str
指向的地址上,但是str
未被初始化,所以该字符串可能被拷贝到任意的地方。
因此得出上面的程序中的1.这里要写字符数组,不能写char* brandName;
这种写法就可以:
char *str = new char[50];
strcpy(str,"The C of Tranquility");
因为先new
了一个内存块,并让str
指向了这块内存,这样就可以把"The C of Tranquility"
拷贝到str
指向的内存块了。
2.第二个参数src
是const char*
类型,它指向一个具体的字符串,也可以说用const char*
可以接收字符串常量
,而const char*
等价于 string
,因此可以将字符串常量赋给string
或者const char*
,将字符串常量赋给char*
一定要记得加const
。
因此得出上面程序中的2.这里要写const char* name = “Munch Box”,不能写char*,因为如果写char*
,编译器会提示“不能把const char*
类型赋给char*
类型”。
strcpy()
的其它属性:
strcpy()
的返回类型是char *
,该函数返回的是一个指向最终的目标字符串 dest
的指针。第一个参数不必指向数组的开始,这个特性可用于拷贝数组的一部分。
#include
#include
using std::cout; using std::cin; using std::endl;
int main(){
char str1[50] = "Be the best that you can be.";
const char* str2 = "beast";
cout << "str1 = " << str1 << endl;//str1 = Be the best that you can be.
cout << "str2 = " << str2 << endl;//str2 = beast
char* ps = str1 + 3;
cout << "ps = " << ps << endl;//ps = the best that you can be.
cout << endl;
ps = strcpy(str1 + 7,str2);//把str2拷贝到str1 + 7的位置,返回值也是str1 + 7的位置
cout << "str1 = " << str1 << endl;//str1 = Be the beast
cout << "str2 = " << str2 << endl;//str2 = beast
cout << "ps = " << ps << endl;//ps = beast 因为ps指向的不是str1,而是str1 + 7
return 0;
}
参考链接:const string 与const string&(C++中的引用)
不带&的是一个常对象,带&是一个常引用,那么什么叫常引用呢?
在讲引用作为函数参数进行传递时,实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。
string constant 和 字符串常量 是一回事,一般的写法如下:
const char* name1 = "marco reus";//别忘了const
string name2 = "cristiano ronaldo";
char name3[50] = "lionel messi";
//char* pt1 = "karim benzema";//这样写是错的!
char* pt2 = name3;//这样写ok
#include
#include
using std::cout; using std::cin; using std::endl;
//第4题:
struct stringy
{
char* pStr;//指向一个字符串
int ct;//计算字符串的长度,不计算结束符'\0'
};
void set(stringy& str,const char* test);
void show(const stringy& str,int n = 1);
void show(const char* test,int n = 1);
int main(){
//第4题:
stringy beany;
char testing[] = "Reality isn't what it used to be.";
set(beany,testing);//beany.pStr指向new的空间
show(beany);cout << "--------" << endl; //这里还会用到beany.pStr
show(beany,2);cout << "--------" << endl;//这里还会用到beany.pStr
delete[] beany.pStr;//用完beany.pStr之后记得释放掉它指向的内存块
testing[0] = 'D';
testing[1] = 'u';
show(testing);cout << "--------" << endl;
show(testing,3);cout << "--------" << endl;
show("Done!");
return 0;
}
void set(stringy& str,const char* test){
str.pStr = new char[strlen(test) + 1];//多new一个字节空间放结束符
strcpy(str.pStr,test);
str.ct = strlen(test);//字符个数,不算结束符
}
void show(const stringy& str,int n){
for(int i = 0;i < n;i++)
cout << str.pStr << endl;//
cout << "字符串中字符的个数为:" << str.ct << endl;
}
void show(const char* test,int n){
for(int i = 0;i < n;i++){
cout << test << endl;
}
cout << "字符串中字符的个数为:" << strlen(test) << endl;
}
1.注意delete[] beany.pStr;
的时机,不能放到set函数中,否则后面在show()函数中调用str.pStr
时,它指向的内存块已经被释放了。
2.直接对char*
类型的指针str.pStr
使用strcpy()
函数,这次就是可以的,是因为new
了一个内存块,并让str.pStr
指向了这块内存:
str.pStr = new char[strlen(test) + 1];//多new一个字节空间放结束符
strcpy(str.pStr,test);//这里可以将test字符串copy给char*指针str.pStr,是因为上一行new了一个内存块,并让`str.pStr`指向了这块内存。
template <class T>
T maxn(T arr[],int n);
template<> char* maxn<char*>(char* pArr[],int n);//具体类型是char* 即char指针,形参是char指针数组
2.题目中的具体化是为char*类型
具体化,返回值也是char*类型
,但参数是个char*数组
,即char* arr[]
,然后自定义char*
数组的时候,前面要加const
,接收返回的char*
时,也要加个const
,因为const指针
可以接收const和非const指针
,
const char* pArr[5] = {"123","123456","12","1234","12345"};//别忘了const
const char* maxStr = maxn(pArr,5);//用constchar指针来接收char*
代码:
#include
#include
using std::cout; using std::cin; using std::endl;
//第6题:
template <class T>
T maxn(T arr[],int n);
template<> char* maxn<char*>(char* pArr[],int n);//具体类型是char* 即char指针,形参是char指针数组
int main(){
//第6题:
int arr1[6] = {20,15,-10,0,65,63};
int max1 = maxn(arr1,5);
cout << "max1 = " << max1 << endl;
double arr2[4] = {-1.1,9.6,3.4,-26.1};
double max2 = maxn(arr2,5);
cout << "max2 = " << max2 << endl;
const char* pArr[5] = {"123","123456","12","1234","12345"};//别忘了const
const char* maxStr = maxn(pArr,5);//用constchar指针来接收char*
cout << "最长的字符串为:" << maxStr << endl;
return 0;
}
template <class T>
T maxn(T arr[],int n){
T max = arr[0];
for(int i = 1;i < n;i++){
if(arr[i] > max)
max = arr[i];
}
return max;
}
template<> char* maxn<char*>(char* pArr[],int n){
int max = 0;
char* maxStr;
for(int i = 0;i < n;i++){
if(strlen(pArr[i]) > max)
max = strlen(pArr[i]);
}
cout << "最长的字符串长度为" << max << endl;
for(int i = 0;i < n;i++){
if(strlen(pArr[i]) == max){
maxStr = pArr[i];
break;
}
}
return maxStr;
}
const char*
类型,它指向一个具体的字符串,也可以说用const char*
可以接收字符串常量
,而const char*
等价于 string
,因此可以将字符串常量赋给string
或者const char*
,将字符串常量赋给char*
一定要记得加const
。
char*
一般指向字符数组,如果它没指向字符数组,也要记得给它初始化!!!
//ok
char *pt1= new char[50];
strcpy(pt1,"The C of Tranquility");
//ok
char name[50];
char *pt2 = name;
strcpy(pt2,"The C of Tranquility");
//不ok:
char *pt3;//指针未初始化!!!
strcpy(pt3,"The C of Tranquility");
(第八章完)
点这里