C++函数

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修饰时,下面两种情况是合理的。

    1. 形参与实参类型不匹配但可以转换为正确类型;
    2. 形参与实参的类型匹配,但是实参是非左值。
  • 上面两条不仅是针对引用的说明,对于普通的函数也有这种操作,也就是说一个函数的形参如果是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的用法是完全相同的

  • 使用模板的局限性:

    1. 如果在函数中定义了赋值操作,但是模板类型是数组,则是不成立的。
    2. 如果在函数中有大于小于号,但模板类是结构,则也不成立。
  • 也就是说模板可能处理不了某些类型(可以通过重载运算符实现),这里有另外一种方法:为特定的数据类型提供具体化的模板定义。
template<> void Swap<float>(float & a, float & b)
{
    cout<<"OK"<<endl;
}
//其中Swap后面的<float>是可选的,这也算是为具体的数据类型具体化定义的一种方式了。
  • 编译器的选择策略:

    1. 创建候选函数列表,其中包含了与被调用函数名称相同的函数和模板函数
    2. 使用候选函数列表创建可行函数列表
    3. 确定是否有最佳的可行函数,如果有则使用它,否则调用出错
  • 另一种选择策略是自己选择,调用的时候直接调用函数即可

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不需要函数
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值