C++函数学习笔记

1 函数声明

  1. 参数传递的语义等同于初始化的语义,参数的类型被逐个检查,如果需要就会做隐式的参数类型转换。和C语言不同,这个机制应该得到重视,因为在C++中会经常用到。
  2. 函数在声明的过程中可以包含参数名,事实上编译器只会记住参数的类型并忽略参数名,参数名的写入只是对读程序有帮助。
  3. 函数的定义中,可以存在不使用的参数。一般出现这种情况说明代码进行过简化或者计划在将来作为功能扩充。
  4. 函数可以用inline修饰,表示这个函数在编译时就会在调用的地方展开(有点像C中的#define())。聪明的编译器应该能对可求值的inline函数求值。
  5. inline函数并不会影响函数的语义,每个inline函数都有自己的独立地址,在线函数里的static变量也有自己的地址。
  6. 对于通过引用或者指针传递进来的参数,我们可以使用const来表示我们不会修改这个参数,以增加程序的可阅读性。因此一般的C++程序规则是,引用(指针)参数加const表示函数不会修改该值,否则函数就会修改该值。最好按规矩来。如:
void f(const int& a)  //这个函数不会对a进行修改
{
  //当然使用const_cast强制转换后进行修改属于没事找抽型
}
  1. 参数传递的语义不同于赋值语义,这一点也十分重要。
  2. 文字量、常量和需要转换的参数可以传递给const&参数,但不能传递给非const的引用。这一规则可以防止很多错误。下面是一个示例:
float fsqrt(const float&);  //带const的引用
void g(double d)
{
  float r = fsqrt(2.0f);    //可以,传递的是保存2.0f的临时量的引用
  r = fsqrt(r);   //可以,传递r的引用
  r = fsqrt(d);   //传递的是保存float(d)临时量的引用
}

void update(float& i);  //不带const的引用(默认可以修改这个值)
void g(double d,float r)
{
  update(2.0f);   //错误,不能传递常量
  update(r);    //可以,传递r的引用
  update(d);    //不可以,不能传递需要转换的量
}
  1. 和C语言中一样,传递给函数数组实际上就是传递数组中首元素的指针。即数组不能按值方式传递,没有形参一说。
  2. C++中main函数可以没有返回值(即使不是void类型)。
  3. 和参数传递语义一样,函数返回值的语义也与初始化的语义相同。如果需要,也会执行相应的类型转换。如:
double f(){return 2;} //int->double隐式转换
  1. 由于void函数没有返回值,因此我们可以将一个void函数作为另外一个void函数的返回值。这一点在写模板时有时有用。如:
void g(int a);
void h(){return g(2);}    //可以

2 函数重载

函数重载机制,允许不同参数类型的函数拥有相同的函数名。这一机制在不同类型对象上执行相同工作时非常有用。如:

void print(int);    //打印int
void print(const char*);    //打印C风格的字符串
  1. 编译器在寻找匹配的最好的重载函数时,如果有两个函数参数类型匹配程度相同,就会报错。如:
void print(double);
void print(long);
void(f){print(2);}  //错误,不知道匹配double还是long
  1. 匹配规则如下:
  2. 准确匹配,不需要转换的匹配(数组名到指针,函数名到函数指针,T到const T的匹配等)。
  3. 提升匹配,即数据精度扩充的匹配(bool到int,char到int,float到double等)。
  4. 标准转换,如int到double,int到unsigned int,T到void等。
  5. 用户自定义转换。如类中使用operator int()将类转化为int等。
  6. 利用函数中声明的省略号的匹配。
  7. 不同作用域里声明的同名函数不算重载,例如:
void f(int);
void g()
{
  void f(double);
  f(2);   //调用f(double),即使f(int)匹配的很好
}
  1. 除了通过显示的类型转换以外,我们也可以使用inline的方式消除匹配过程中遇到的歧义问题。如inline void f1(int n){f1(long(n));}在参数为int类型时就会调用参数为double的那个重载函数。

3 默认参数

一个通用的函数其输入参数往往很多,但是在简单的调用中我们不想输入特别多的参数。这就需要我们有一种默认机制自动的帮我们补上不想设置的参数。比如我们输出一个整数,我们希望可以默认以十进制进行输出,但也可以设置为任意进制进行输出。

void print(int value,int base = 10);    //默认基为10
void f()
{
  print(20);    //以10为基输出
  print(20,16); //以16为基输出
}

当然上面的效果也可以通过前面所说的重载机制实现,但是意图不是那么明显。

  1. 默认机制只对排在后面的哪些参数提供默认参数。
int f(int,int = 0,char* = 0);   //可以
int g(int = 0,int,char*);   //错误

上面的* =中的空格必须要有,因为*=是乘等赋值运算符。
2. 同一个作用域中的默认声明不能重复或者改变。

void f(int x = 1);
void f(int = 1);    //错误,重复
void f(int =8);     //错误,不能改变

void g()
{
  void f(int x = 9);    //可以,这个声明将遮盖外层的声明
}

4 未确定参数数目的函数

对于有些函数来说,我们可能没有办法确定具体的参数数目,这时我们可以用...表示可能还有一些参数。如int printf(const char* ...).由于参数的输入个数不确定,这些参数会跳过类型检查,所以应该尽可能少的使用未确定参数的函数。
里提供了一组标准宏,用来访问这些未加描述的参数。下面的error函数是一个使用例子,假设error函数最后一个参数为0.

void error(int severity ...)
{
  va_list ap;
  va_start(ap,severity);    //arg开始
  
  for(;;)
  {
    char* p = va_arg(ap,char*);
    if(p == 0) break;
    cerr<< p <<'';
  }
  va_end(ap);     //arg清理
  cerr<<endl;
  if(severity)
    exit(severity);
}

首先,通过调用va_start()定义并初始化一个va_list。宏va_start以一个va_list的名字和函数的最后一个有名形式参数的名字作为参数。宏va_arg()用于按顺序提取出各个无名参数。在每次调用va_arg()时,程序员必须提供一个类型,va_arg()将按这种类型去传递参数。最后,一个使用过va_start()的函数在退出之前,必须调用一次va_end()来恢复堆栈。

5 指向函数的指针

每个函数都有它对应的地址,我们可以通过取一个函数的地址并赋给一个对应的函数指针,然后通过函数指针去调用这个函数。

void error(string s);
void (&efp)(string);    //函数指针
void f()
{
  efp = &error;     //&可以省略不写
  (*efp)("error!");   //*可以省略不写
}

efp后面的括号表明要调用这个函数指针的函数,所有*可以省略不写。同样函数名也相当于指针,所以赋值时也可以不写。
函数指针的参数类型和函数声明的必须要完全一致才行。不存在重载的匹配机制。
可以使用typedef为指向函数的指针类型定义一个名字。

typedef void (*SIG_TYP)(int);     //取自<signal.h>
typedef void (*SIG_ARG_TYP)(int);

SIG_TYP signal(int,SIG_ARG_TYP);      //声明一个函数,函数的输入参数和输出参数都有函数指针

指向函数的指针的数组常常很有用,例如,下面这个鼠标编辑器菜单系统的例子,其中的函数表示各种各样的操作(没给出具体实现)。

typedef void (*PF)();

PF edit_ops[] = {   //编辑操作
  &cut,&paste,&copy,&search  
};

FP file_ops[] = {   //文件管理
  &open,&append,&close,&write
};

//定义并初始化一些指针,由它们去控制各种操作
PF* button2 = edit_ops;
PF* button3 = file_ops;  

...
button2[2]();     //调用按键2的第三个函数(copy函数)

指向函数的指针可以用于提供一种简单形式的多态性例程,即那种可以应用于许多不同类型的对象的例程,考虑下面的Shell排序算法。

typedef int (*CFT)(const void*,const void*);

// shell排序
// 对向量base的n个元素按照递增顺序排序
// 用由"cmp"所指的函数做比较(parm1<parm2返回负值,相等返回0,否则返回正值)
// 元素的个数是"n"
// 元素的大小是"sz"
void ssort(void* base,size_t n,size_t sz,CFT cmp)
{
  for(int gap = n/2;0 < gap;gap /= 2)
  {
    for(int i = gap;i < n;i++)
    {
      for(int j=i-gap;0 <= j;j -= gap)
      {
        char* b = static_cast<char*>(base);   //必须强制
        char* pj = b + j*sz;        //&base[j]
        char* pjg = b+ (j+gap)*sz;    //&base[j+gap]
        
        if(cmp(pjg,pj) < 0)
        {
          for(int k = 0;k < sz;k++)
          {
            char temp = pj[k];
            pj[k] = pjg[k];
            pjg[k] = temp;
          }
        }
      }
    }
  }
}

下面是使用上面排序算法的一个示例,使用同一个排序函数分别对不同的字段进行排序。


#include <iostream>
#include <cstddef>    //size_t
#include <cstring>

using namespace std;

struct User
{
  const char* name;
  const char* id;
  int dept;
};

User heads[] = {
  "Richie D.M",     "dmr",    11271,
  "Sethi R.",       "ravi",   11272,
  "Szymanski T.G",  "tgs",    11273,
  "Schryer N.L.",   "nls",    11274,
  "Schryer N.L.",   "nls",    11275,
  "Kernigham B.W",  "bwk",    11276
};

typedef int (*CFT)(const void*,const void*);

void ssort(void* base,size_t n,size_t sz,CFT cmp);
void print_id(User* v,int n);
int cmp1(const void* p,const void* q);    //比较名字串
int cmp2(const void* p,const void* q);    //按照部门编号比较

int main()
{
  cout<<"按照名字字母顺序排序:"<<endl;
  ssort(&heads,6,sizeof(User),cmp1);
  print_id(heads,6);
  cout<<endl;
  cout<<"按照部门编号排序:"<<endl;
  ssort(&heads,6,sizeof(User),cmp2);
  print_id(heads,6);
}

//排序
void ssort(void* base,size_t n,size_t sz,CFT cmp)
{
  for(int gap = n/2;0 < gap;gap /= 2)
  {
    for(int i = gap;i < n;i++)
    {
      for(int j=i-gap;0 <= j;j -= gap)
      {
        char* b = static_cast<char*>(base);   //必须强制
        char* pj = b + j*sz;        //&base[j]
        char* pjg = b+ (j+gap)*sz;    //&base[j+gap]
        
        if(cmp(pjg,pj) < 0)
        {
          for(int k = 0;k < sz;k++)
          {
            char temp = pj[k];
            pj[k] = pjg[k];
            pjg[k] = temp;
          }
        }
      }
    }
  }
}

//打印结果
void print_id(User* v,int n)
{
  for(int i = 0;i < n;i++)
    cout<<v[i].name<<"	"<<v[i].id<<'	'<<v[i].dept<<endl;
}

//比较函数:比较名字串
int cmp1(const void* p,const void* q)
{
  return strcmp(static_cast<const User*>(p)->name,static_cast<const User*>(q)->name);
}

//比较函数:比较部门编号
int cmp2(const void* p,const void* q)
{
  return static_cast<const User*>(p)->dept - static_cast<const User*>(q)->dept;
}

如果将函数指针指向了一个重载函数,函数指针的类型将决定选择哪个重载函数。

void f(int);
int f(char);

void (*pf1)(int) = &f;      //void f(int)
int (*pf2)(char) = &f;      //int f(char)
void (*pf3)(char) = &f;     //错误,没有void f(char)

在使用函数指针之前先考虑使用虚函数或者模板会不会更好。

6 宏

和C语言中不同,在C++中极少会使用到宏。宏定义的变量使用const类型,带参数的宏可以使用inline函数。这些机制会更加的安全。

#define BUFFER_SIZE 10
const int BUFFER_SIZE = 10;

#define MAX(x,y) ((x>y)?x:y)
template<class T> inline T MAX(T x,T y){return (x>y)?x:y}
  1. 宏不能重载,也不能递归调用。
  2. 为了防止宏在展开时出错,最好将所有的宏都用()括起来。并且如果宏操作的是全局变量,要使用作用域解析符::。
#define SQRT(a) a*a     /*没有括起来*/
#define INC_x (x)++     /*让全局变量x增加*/

int x = 0;    //全局计数器
void f()
{
  int x = 3;    //临时的局部变量
  int y = SQRT(x+2);  //展开出错,y=x+2*x+2而不是(x+2)*(x+2)
  INC_x;    //并没有增加了全局的计数器
}
  1. 使用template,enum和namespace机制都能很好的替代宏,也更加安全。
  2. 通过宏拼接运算符##可以拼接两个串,由于宏是在编译时展开,可以使用这个机制来产生一个变量或函数。
#define VAR_NAME(a,b) a##b  

int VAR_NAME(Adc,Start)();  //将会执行AdcStart()函数
  1. 可以使用#undef X来去除一个宏,无论之前有没有定义过X。
  2. 即使在C++中也几乎不会避免使用的宏——条件编译#ifnef XXX ... #endif#ifdef XXX ... #endif.C语言中用的太多了就不介绍了。
原文地址:https://www.cnblogs.com/yabin/p/6407829.html