C++ Primer Plus读书笔记
函数部分
1.基本概念
- 函数在代码中需要有原型和实现两部分组成,其中的原型中需要包含函数的参数情况,原型和实现的参数名称可以不相同,但是类型必须相同,甚至原型中可以不写参数的名字
void myFunction(int, char);
函数的参数按值传递,参数是复制过来的新的局部变量,但是如果这里使用引用的话,则可以修改原来的值。
形参与实参:形参指的是接收传递值的变量,而实参指的是传递给函数的值。
- 指针与数组的关系:数组的索引,就是数组第一个元素的地址,是一个指针。因此在函数进行传递的时候,接收函数可以直接写成指针。
int group[10];
group 与 &group[0] 表示的是同一个东西。
2.关于const
- const是一个用来限定内容不能被修改的变量,其中,const的位置不同,所限定的内容也是不同的,const将离他最近的量加以限制
#include<iostream>
using namespace std;
int main()
{
int a = 1;
int b = 3;
//第一种情况
//q是一个指向int的指针,因为const离他最近,
//因此q这个指针本身是不可以变化的,但是可以改变这个指针
//所指向的值,下面这两个语句是正确的。
int * const q = &a;
*q=b;
//第而种情况
//r是一个指向int的指针,但是const限定的内容是
//r这个指针指向的数据,因此r指向的内容是不可变化的。
//但是可以通过改变指针的内容来改变值,下面这两个语句是正确的。
const int * r = &a;
r=&b;
}
3.函数指针
与数据项类似,函数也有地址,函数的地址是存储起机器语言代码的内存开始的地址。可以编写将另一个函数的地址作为参数的函数,这样第一个函数能找到第二个函数,并运行它。
一些名字很像的函数在调用的时候如果不太方便,则可以通过这种方式进行更为简化的调用。
这部分需要注意的地方是进行函数指针的声明的时候,需要将星号*和函数名用括号括起来,否则表示返回值是一个指针。
//声明函数指针,参数和返回值类型都必须相匹配
int first(double);
double second(int);
int (*test)(int);
test = first//错误,参数值类型不一样
test = second//错误,返回值类型不一样
- 可以将函数指针作为函数的参数进行传递
double first(int a)
{
return a/1.0;
}
double second(int a)
{
return a/2.0;
}
int choose(double (*fp)(int))
{
int a = 3;
return (*fp)(a);
}
int main()
{
cout<<choose(first);
cout<<choose(second);
return 0;
}
4.内联函数与宏定义
内联函数在声明和定义之前需要添加关键字inline,与常规函数的区别在于:常规函数调用会使程序调到另一个地址执行函数的内容,在函数结束时返回,如果将函数内联进来,则会通过花费更多内存的代价将函数复制成若干份,嵌入在程序之中。如果执行一个函数所需要的时间小于调用的时间,则可以将这个函数写成内联函数的形式。
内联与宏定义:在C语言中是没有内联函数这一说的,而是使用宏定义来完成相似的功能,
#define SQUARE(X) ((X)*(X))
使用宏定义的时候一定要加上各种括号,每一个小部分都要加括号,因为X可能会是很复杂的内容,比如X可以取1+1,这样,如果不加括号则会出现预期不到的错误。
5.引用变量
引用变量:是已定义变量的别名,引用变量的作用还是用来作为函数的形参。
形参为引用,而且被const修饰时,下面两种情况是合理的。
- 形参与实参类型不匹配但可以转换为正确类型;
- 形参与实参的类型匹配,但是实参是非左值。
- 上面两条不仅是针对引用的说明,对于普通的函数也有这种操作,也就是说一个函数的形参如果是const修饰过的,则如果满足上面两种情况时也是可以执行的。
- 左值:左值指的是可以被引用的数据对象,例如变量、数组元素、结构成员、引用和解除引用的指针都是左值
- 非左值:指的是字面常量,用引号括起来的字符串除外。
void first(int &a)
{
a = 3;
}
void second(const double & a)
{
cout<<"hello"<<endl;
}
int main()
{
int a = 2;
first(a);
cout<<a<<endl;
second(a); //正确的,类型虽然不匹配但是可以转换
second(3.5); //正确的,类型相同,但是非左值
return 0;
}
//这个函数输出的结果是3,因此将引用当做形参是很方便的
将引用用于结构和类(二者类似)
- 引用适合于结构和类,引入引用的主要目的就是为了用于这些类型的
返回引用的原因:如果返回的是一个结构,而不是引用,则会将整个结构复制到一个临时位置,再将这个拷贝复制给左值。
返回与传统的按值传递是类似的,计算关键字return后面的表达式,并将结果返回给调用函数,从概念上说,这个值被复制到一个临时位置,而调用程序将使用这个值。
- 相反的,如果使用返回引用,则不会将这个值拷贝到临时变量,而是直接将这个值复制到调用函数的左值上。
#include<iostream>
using namespace std;
int & first(const int a)
{
cout<<a<<endl;
int b = 7;
return b;
}
// 注意这里返回的b是一个临时变量,因此当这个函数结束之后,b就消失了,这样编译器会报warning而执行时会出错。
int main()
{
float a = 2;
int c;
c = first(a);
cout<<c<<endl;
return 0;
}
- 上面代码提到的问题,一种解决办法是:返回一个作为参数传递给函数的引用。
int& abc(int a, int b, int c, int& result)
{
result = a + b + c;
return result;
}
- 另一种解决方法是用new来分配新的存储空间
const int & first(const int a)
{
cout<<a<<endl;
int *b = new(int);
*b = 7;
return *b;
}
- 将const用于引用返回类型:常规的非引用返回类型是“右值”,是不能通过地址访问的值,这种返回值位于临时内存单元中,当运行到下一跳语句时,他们可能就不再存在。
- 也是用const来修饰返回的指针或引用,保护指针指向的内容或引用的内容不被修改,也常用于运算符重载。归根究底就是使得函数调用表达式不能作为左值。
#include<iostream>
using namespace std;
class A {
private:
int i;
public:
A(){i=0;}
const int & get(){
return i;
}
};
int main(){
A a;
cout<<a.get()<<endl; //数据成员值为0
a.get()=1; //尝试修改a对象的数据成员为1,而且是用函数调用表达式作为左值。
cout<<a.get()<<endl; //数据成员真的被改为1了,返回指针的情况也可以修改成员i的值,所以为了安全起见最好在返回值加上const,使得函数调用表达式不能作为左值
return 0;
}
- 从上面代码运行的结果可以看出,如果get方法前面加一个const,则编译是不能通过的,主要是a.get()=1这一句是错误的,加了const之后这个返回值是不能作为左值的。
int q = a.get();
q = 2;
- 使用上面这两行代码也是没有办法更改i的。对结构体来说也是类似的。
6.函数模板
使用泛型来定义函数,其中的泛型可以用具体的类型来替换,通过将类型作为参数传递给模板,可以使编译器生成该种类型的函数。
使用typename创建模板:
template<typename Type>
void Swap(Type & a, Type & b)
{
Type temp;
temp = a;
a = b;
b = temp;
}
//直接用就可以啦,但是这里的a和b的类型必须相同。
还有另一种创建的方法,class。它与typename的用法是完全相同的
使用模板的局限性:
- 如果在函数中定义了赋值操作,但是模板类型是数组,则是不成立的。
- 如果在函数中有大于小于号,但模板类是结构,则也不成立。
- 也就是说模板可能处理不了某些类型(可以通过重载运算符实现),这里有另外一种方法:为特定的数据类型提供具体化的模板定义。
template<> void Swap<float>(float & a, float & b)
{
cout<<"OK"<<endl;
}
//其中Swap后面的<float>是可选的,这也算是为具体的数据类型具体化定义的一种方式了。
编译器的选择策略:
- 创建候选函数列表,其中包含了与被调用函数名称相同的函数和模板函数
- 使用候选函数列表创建可行函数列表
- 确定是否有最佳的可行函数,如果有则使用它,否则调用出错
另一种选择策略是自己选择,调用的时候直接调用函数即可
Swap(a,b)
Swap<float>(a,b)//手动选择
//尖括号中的类型可以随意选择
实例化和具体化:在代码中包含函数模板本身不会生成函数定义,他只是一个用于生成函数定义的方案。
- 实例化:编译器使用模板为特定类型生成函数定义时,得到的是模板实例。模板并非函数定义,但是使用某种类型的模板实例是函数定义,这种实例化方法被称为隐式实例化
template<typename T> void Swap(T &t1,T &t2) { T _t; _t=t1; t1=t2; t2=_t; }
调用
int i = 10,j=20; //隐式实例化:在调用的时候自己生成 Swap(i,j); //显式实例化:加上template并指定类型 template void Swap<int>(int &t1,int &t2);
- 显式具体化的原型和定义应以template<>打头,并通过名称来指出类型。显式具体化优先于常规模板,而非模板函数优先于具体化和常规模板
//显式实例化 template void Swap<job>(job &c1,job &c2); //显式具体化 template<> void Swap(job &c1,job &c2); //二者是相同的,区别就在于,显式具体化声明在关键字template 后加<>,显式实例化没有
- 实例化:编译器使用模板为特定类型生成函数定义时,得到的是模板实例。模板并非函数定义,但是使用某种类型的模板实例是函数定义,这种实例化方法被称为隐式实例化
- 如果模板想用多个类型,则这么写
template<class T1, class T2>
- 但是这样可能会产生类型不匹配的问题,比如在新函数中要两个函数相加,则没办法得知最终到底是哪种(一个int加一个float在函数中没办法声明他的合属于哪种类型)
- 这时候需要用到decltype(C++11新标准)
delctype(x+y) z = x + y;
- delctype与auto还是不一样的,auto必须要初始化,而delctype不需要函数