C/C++编程:函数

1059 篇文章 285 订阅

两种函数

  • 在类范围中定义的函数叫做成员函数
  • 否则就叫做非成员函数/自由函数

函数声明&定义

函数声明

作用:函数声明描述了函数到编译器的接口。也就是说,它将函数返回值类型以及参数的类型和数量告诉编译器。

函数声明的例子:

int sum(int a, int b);

函数声明的必须部分:

  • 返回类型:
    • void:表示不返回任何值
    • auto(C++11起):表示编译器从return语句推断类型
    • decltype(auto)(c++14起):类型推导返回类型
  • 函数名:
    • 必须以字母或下划线开头,不能包含空格
    • 一般而言,标准库函数名中的前导下划线指示私有成员函数,不能由用户调用
  • 参数列表:
    • 一组用大括号限定、逗号分隔的零个或多个参数
    • 指定类型以及可以用于在函数体内访问值的可选局部变量名。

函数声明的可选部分

  • constexpr:指示函数的返回值是一个常数值,可以在编译时计算
constexpr float exp(float x, int n)
{
    return n == 0 ? 1 :
        n % 2 == 0 ? exp(x * x, n / 2) :
        exp(x * x, (n - 1) / 2) * x;
};
  • 链接规范:extern/static
//Declare printf with C linkage.
extern "C" int printf( const char *fmt, ... );
  • inline:指示编译器将对函数的每个调用替换为函数代码本身。在某个函数快速执行并且在性能关键代码段中重复调用的情况下,内联可以帮助提高性能
inline double Account::GetBalance()
{
    return balance;
}
  • noexcept表达式:指定函数是否会引发异常:
#include <type_traits>

template <typename T>
T copy_object(T& obj) noexcept(std::is_pod<T>) {...}
  • cv限定符(仅用于成员函数):指定该函数是const还是volatile
  • virtual 、 override 或 final(仅用于成员函数)
    • virtual:指定可以在派生类中重写函数
    • overried:表示派生类中的函数正在重写该函数
    • finial:表示不能在任何进一步的派生类中重写函数
  • static(用于成员函数):表示该成员函数不与类的任何对象实例相关联
  • ref 限定符(仅非静态成员函数):指定编译器在隐式对象参数(*this)为右值引用与左值引用时要选择的函数的重载

函数定义

函数定义 = 声明 + 主体,例子:

int sum(int a, int b)
{
    return a + b;
}

函数体内声明的变量称为局部变量。 它们会在函数退出时超出范围;因此,函数应永远不返回对局部变量的引用!

const与constexpr函数

常量成员函数

可以将成员函数声明为const,作用是

  • 指示该函数是一个“只读”函数,该函数不会修改为其调用该函数的对象
  • 常量成员函数不能修改任何非静态数据成员或者调用不是常量的任何成员函数
  • 可以帮助编译器强制实施const正确性。如果有人错误地尝试使用声明为的函数修改对象 const ,则会引发编译器错误。
// constant_member_function.cpp
class Date
{
public:
   Date( int mn, int dy, int yr );
   int getMonth() const;     // A read-only function
   void setMonth( int mn );   // A write function; can't be const
private:
   int month;
};

int Date::getMonth() const
{
   return month;        // Doesn't modify anything
}
void Date::setMonth( int mn )
{
   month = mn;          // Modifies data member
}
int main()
{
   Date MyDate( 7, 4, 1998 );
   const Date BirthDate( 1, 18, 1953 );
   MyDate.setMonth( 4 );    // Okay
   BirthDate.getMonth();    // Okay
   BirthDate.setMonth( 4 ); // C2662 Error
}

constexpr函数

将函数声明为constexpr,该函数的返回值可在编译时确定。Constexpr 函数的执行速度通常比常规函数快。

constexpr float exp(float x, int n)
{
    return n == 0 ? 1 :
        n % 2 == 0 ? exp(x * x, n / 2) :
        exp(x * x, (n - 1) / 2) * x;
}

参数

值传参与引用传参

默认缺省参数

  • 默认参数从右向左处理。函数传参是压栈,从左到右,因此,默认参数要放到最右边
  • 默认参数必须放在右边,中间不能有不默认的参数
  • 函数指针参数没有默认的。
#include <iostream>

using namespace std;

void go()
{

}

void go(int a)
{

}
void go(int a, int b)
{
}

void go(int a, int b, double c = 14.0)
{

}
int main()
{
	//void(*p)(int a, int b, double c = 14.0) = go;  //函数指针中不能有默认参数
	void(*p)(int a, int b, double c ) = go;
	void(*p1)(int , int , double ) = go;
}

变参函数

返回值

C++对返回值的类型有一定的限制:不能是数组,可以是其他任何对象。如果非要返回数组,请将数组作为结构会在对象组成部分来返回:

#include <iostream>

struct aa{
    int *a;
};
struct aa test(){
    int arr[10] = {1, 2};

    struct aa a{};
    a.a = arr;
    return a;
}

int main(){
    printf("%d", test().a[0]);
}

函数是如何返回值的:函数通过将返回值复制到指定的CPU寄存器或者内存单元中来将其返回。随后,调用程序将查看该内存单元。返回函数和调用函数必须就该内存单元中存储的数据的类型达成一致。
在这里插入图片描述

显式默认设置的函数和已删除的函数

作用

  • 在C++11中,默认函数和已删除函数使你可以显式控制是否自动生成默认特殊成员函数。
  • 已删除函数还可以为你提供简单语言,以防所有类型的函数(特殊成员函数、普通成员函数、非成员函数)的自变量中出现由问题的类型提升,这会导致意外的函数调用

规则

  • 在 C++ 中,如果某个类型未声明它本身,则编译器将自动为该类型生成默认构造函数、拷贝构造函数、拷贝赋值运算符和析构函数。 这些函数称为特殊成员函数,它们使C++中简单的用户定义类型的行为类似C中的结构。也就是说,你可以创建、复制和销毁它们,而无需任何其他的编码工作。C++11 会将移动语义引入语言中,并将移动构造函数和移动赋值运算符添加到编译器可自动生成的特殊成员函数的列表中。
  • 此外,C++编译器还会为以下这些自定义类型提供默认操作符函数:operator、operator&、operator &&、operator *、operator ->、operator ->*、operator new、operator delete
  • 这对于简单类型非常方便,但是复杂类型通常自己定义以后或者多个特殊成员函数,这可以阻止自动生成其他特殊函数。实践中
    • 如果显式声明了任何构造函数,则不会自动生成默认构造函数
    • 如果显式声明了虚拟析构函数,则不会自动生成默认析构函数
    • 如果显式声明了移动构造函数或者移动赋值运算符,则不自动生成复制构造函数、复制赋值运算符
    • 如果显式声明了复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符或析构函数,则:不自动生成移动构造函数、移动赋值运算符

此外,C++11标准指定以下附加规则:

  • 如果显式声明了复制构造函数或析构函数,则不会自动生成复制赋值运算符
  • 如果显式声明了复制赋值运算符或析构函数,则不会自动生成复制构造函数

这些规则的结果也可能泄露到对象层次结构中。比如,如果处于任何原因,基类无法具有可从派生类调用默认构造函数(即 public protected 不带任何参数的或构造函数),则从派生的类不能自动生成其自己的默认构造函数。

这些规则可能会使本应直接的内容、用户定义类型和常见 C++ 惯例的实现变得复杂。

  • 一旦声明了自定义版本的构造函数,则有导致我们定义的类型不再是POD的了,这意味着编译器失去了优化简单的数据结构的可能,因此,我们需要一些方式来使得简单类型“恢复POD”的特质

在C++中,标准是通过提供了新的机制来控制默认版本函数的生成来完整合格目标的。这个新机制重用了default关键字。可以在默认函数定义或者声明时加上default,从而显式的指示编译器生成该函数的默认版本。而如果指定产生默认版后,程序不再也不应该实现一个同名的函数。此时自定义类型依旧是POD类型。

struct Two{
public:
	Two() = default;
	Two(int i) : data(i){}
private:
	int data;
};
  • 另外,我们可能希望在一些情况下能够限制一些默认函数的生成。典型的是禁止类的拷贝构造

通过以私有方式复制构造函数和复制赋值运算符,而不定义它们,使用户定义类型不可复制。

struct noncopyable
{
  noncopyable() {};

private:
  noncopyable(const noncopyable&);
  noncopyable& operator=(const noncopyable&);
};

在 C++11 中,不可复制的习语可通过更直接的方法实现。

struct noncopyable
{
  noncopyable() =default;
  noncopyable(const noncopyable&) =delete;
  noncopyable& operator=(const noncopyable&) =delete;
};

具体参见:C/C++编程:noncopyable原理及实现

显式默认设置的函数

C++11标准将“=default”修饰的函数称为显式缺省函数

可以默认设置任何特殊成员函数:

  • 显式声明特殊成员函数使用默认实现
    • 请注意,您可以在类的正文之外默认使用特殊成员函数,只要它是内联。
    • 由于普通特殊成员函数的性能优势,因此我们建议你在需要默认行为时首选自动生成的特殊成员函数而不是空函数体。
struct widget
{
  widget()=default;

  inline widget& operator=(const widget&);
};

inline widget& widget::operator=(const widget&) =default;
  • 定义具有非公共访问限定符的特殊成员函数
  • 恢复其他情况下被阻止其自动生成的特殊成员函数。

已删除的函数

  • 可以删除特殊成员函数以及普通成员函数和非成员函数、以阻止定义或者调用它们。
  • 删除特殊成员函数提供了一种使编译器无法生成不需要的特殊成员函数的更清晰的方式
  • 用处:编码编译器做一些不必要的隐式数据转换
class ConvType{
public:
    ConvType(int i){};
    ConvType(char a) = delete; //删除char版本
};

void Func(int ct){}
void Func(char ct) = delete ;

int main(){
    Func(3);
    Func('a'); // 无法通过编译

    ConvType c(3);
    ConvType d('a');// 无法通过编译
}
  • 用于自定义类:显式删除自定义类型的operator new操作符的话,可以做到避免在堆上分配该对象:
#include <cstdio>

class NoHeapAlloc{
public:
    void *operator new(std::size_t) = delete;
};

int main(){
    NoHeapAlloc nha;
    NoHeapAlloc *pb = new NoHeapAlloc;
}
  • 在一些情况下,我们需要对象在指定内存位置进行内存分配,并且不需要析构函数来完成一些对象级别的清理。这个时候,我们可以通过显式删除析构函数来限制自定义类型在栈上或者静态的构造。
#include <cstdio>
#include <new>

extern void *p;

class NoStackAlloc{
public:
    ~NoStackAlloc() = delete;
};

int main(){
    NoStackAlloc nsa; // 无法通过编译
    new(p) NoStackAlloc(); // 布局new,假设p无需调用析构函数
}

由于布局new构造的对象,编译器不会为其调用析构函数,因此析构函数被删除的类能正常的构造。推导:将显式删除析构函数还可以用于构建单例模式

  • 必须在声明函数时将其删除;不能在这之后通过声明一个函数然后不再使用的方式来将其删除。

  • 删除普通成员函数或非成员函数可阻止有问题的类型提升导致调用意外函数。 这可发挥作用的原因是,已删除的函数仍参与重载决策,并提供比提升类型之后可能调用的函数更好的匹配。 函数调用将解析为更具体的但可删除的函数,并会导致编译器错误。

void call_with_true_double_only(float) =delete;
void call_with_true_double_only(double param) { return; }
  • 请注意,在前面的示例中, call_with_true_double_only 通过使用 float 参数调用将导致编译器错误,但 call_with_true_double_only int 不会使用参数调用; 在 int 这种情况下,自变量将从提升 int 到, double 并成功调用 double 函数的版本,即使这可能不是预期的。 若要确保使用非双精度参数对此函数进行的任何调用都将导致编译器错误,可以声明已删除的函数的模板版本。
template < typename T >
void call_with_true_double_only(T) =delete; //prevent call through type promotion of any T to double from succeeding.

void call_with_true_double_only(double param) { return; } // also define for const double, double&, etc. as needed.

函数指针

C++通过与C语言相同的方式支持函数指针。但是更加类型安全的替代方法是使用函数对象

如果声明返回函数指针类型的函数,则建议使用typedef来声明函数指针类型的别名:

typedef int (*fp)(int);
fp myFunction(char* s); // function returning function pointer

如果不执行此操作,则函数声明的正确语法可以通过用函数名称和自变量列表替换标识符(上例中为 fp)来从函数指针的声明符语法推导出,如下所示:

int (*myFunction(char* s))(int);

前面的声明等效于上面使用的声明 typedef 。

函数模板

函数模板类似于类模板:它基于模板自变量生成具体功能。在虚构情况下,模板能够推断类型参数,因此无需显式的指定它们:

template<typename Lhs, typename Rhs>
auto Add2(const Lhs& lhs, const Rhs& rhs)
{
    return lhs + rhs;
}

auto a = Add2(3.13, 2.895); // a is a double
auto b = Add2(string{ "Hello" }, string{ " World" }); // b is a std::string

函数重载

  • C++ 允许同一范围内具有相同名称的多个函数的规范。 这些函数称为 重载 函数。

  • 重载函数使你能够为函数提供不同的语义,具体取决于参数的类型和数量。

函数声明元素是否用于重载
函数返回类型
自变量的数量
自变量的类型
省略号存在或者缺失
名称的使用typedef
未指定的数组边界
const或者volatile是,应用于整个函数时
引用限定符
  • 不能为重载运算符提供默认参数

  • 默认参数不被是为函数类型的一部分。因此,它不用于选择重载。尽在默认自变量上存在差异的两个函数被视为多个定义非不是重载函数
    在这里插入图片描述

  • 对于区分重载函数,类型 “数组 of” 和 “指向” 被认为是相同的,但仅适用于单个经过维度的数组。 这就是这些重载函数冲突的原因,并生成错误消息:
    在这里插入图片描述

  • 对于多维数组,第二个和后续维度被视为类型的一部分。 因此,它们可用来区分重载函数:
    在这里插入图片描述

  • 对于typedef
    在这里插入图片描述

  • 对于引用:
    在这里插入图片描述

  • 重载可以区分constvoltile引用

// argument_type_differences.cpp
// compile with: /EHsc /W3
// C4521 expected
#include <iostream>

using namespace std;
class Over {
public:
   Over() { cout << "Over default constructor\n"; }
   Over( Over &o ) { cout << "Over&\n"; }
   Over( const Over &co ) { cout << "const Over&\n"; }
   Over( volatile Over &vo ) { cout << "volatile Over&\n"; }
};

int main() {
   Over o1;            // Calls default constructor.
   Over o2( o1 );      // Calls Over( Over& ).
   const Over o3;      // Calls default constructor.
   Over o4( o3 );      // Calls Over( const Over& ).
   volatile Over o5;   // Calls default constructor.
   Over o6( o5 );      // Calls Over( volatile Over& ).
}

重载实现动多态(overried、final)

  • 一个类A中声明的虚函数func在其派生类中再次被定义,而且B中的函数func跟A中的原型一样(函数名、参数列表等),那么我们就称B重载了A的func函数。对于任何B类型的变量,调用成员函数func都是调用B重载的版本。而如果同时有A的派生类C,却没有重载A的fun函数,那么调用成员函数fun则会调用A中的版本。这在C++中就实现为多态
  • 在通常情况下,一旦在基类A中的成员函数fun被声明为virtual时,那么对其派生类B而言,fun总是能被重载的(除非被重写了)。有时我们并不想fun在B类型派生类中被重载,那么C++98中没有办法对此进行限制,所以C++11引入了final这个关键字来组织函数重写。final关键字的作用是使得派生类不可覆盖它所修饰的函数,如下:
struct 	Object{
	virtual void func() = 0;
};
struct Base : public Object{
	void func() final; // 声明为final
};

struct Derived : public Base{
	void func(); // 无法通过编译
};
  • 上面的final是描述一个派生类的,当然,我们也可以用final来描述基类中的虚函数(不要这样做!!!)。不过这样的虚函数就无法重载了,也就失去了虚函数的意义。如果不想成员函数被重载,程序员可以直接将成员函数定义为非虚的。而final通常只在继承关系的“中途”终止派生类的重载中有意义,这就给面向对象的程序员带来了更大的控制力。
  • 在C++中重载还有一个特点,就是对于基类声明为virtual的函数,之后的重载版本都不需要再声明该重载函数为vritual。即使在派生类中声明了virtual,编译器也会把它忽略掉。 这会带来两个问题,一个就是无法直接从代码中看出是不是虚函数,另外在C++中虚函数可能会“跨层”,没有在父类中声明的接口有可能是祖先的虚函数接口。这给我们编写继承结构复杂的类的编写带来了困难。
  • 为了解决上面的问题,C++11中引入了虚函数描述符override如果派生类在虚函数声明时使用了override描述符,那么该函数就必须重载其基类中的同名函数,否则代码将无法通过编译。我们来看个例子:
struct Base{
	virtual void Turing() = 0;
	virtual void Dijkstra() = 0;
	virtual void VNeumann(int g) = 0;
	virtual void DKnuth() const;
	void Print();
};

struct DerivedMid : public Base{
	// 隔离一个接口
};

struct DeviedeTop : public DerivedMid {
	void Turing() override;
	void Dijkstre() override; // 无法通过编译:拼写错误,并非重载
	void VNeumann(double g) override; // 无法通过编译,参数不一致,并非重载
	void DKnuth() override; // 无法通过编译,常量性不一致,并非重载
	void Print() override; // 无法通过编译,非虚函数重载
};

  • 从上面我们可以看出,override修饰符可以保证编译器辅助的做一些检查

成员函数的 Ref 限定符

通过引用限定符,可以根据指向的对象 this 是右值还是左值,来重载成员函数。 当你选择不提供对数据的指针访问权限时,可以使用此功能来避免不必要的复制操作。 例如,假设类 C 在其构造函数中初始化某些数据,并在成员函数中返回该数据的副本 get_data() 。 如果类型为的对象 C 是要销毁的右值,则编译器将选择 get_data() && 重载,这会移动数据而不是复制数据。

#include <iostream>
#include <vector>

using namespace std;

class C
{

public:
    C() {/*expensive initialization*/}
    vector<unsigned> get_data() &
    {
        cout << "lvalue\n";
        return _data;
    }
    vector<unsigned> get_data() &&
    {
        cout << "rvalue\n";
        return std::move(_data);
    }

private:
    vector<unsigned> _data;
};

int main()
{
    C c;
    auto v = c.get_data(); // get a copy. prints "lvalue".
    auto v2 = C().get_data(); // get the original. prints "rvalue"
    return 0;
}

函数重载原理

根据参数类型委托不同的函数

比如实际上void go()–>go
void go(int a, int b)—>go_int_int
void go(int a)—>go_int函数

// function_overloading.cpp
// compile with: /EHsc
#include <iostream>
#include <math.h>
#include <string>

// Prototype three print functions.
int print(std::string s);             // Print a string.
int print(double dvalue);            // Print a double.
int print(double dvalue, int prec);  // Print a double with a
                                     //  given precision.
using namespace std;
int main(int argc, char *argv[])
{
    const double d = 893094.2987;
    if (argc < 2)
    {
        // These calls to print invoke print( char *s ).
        print("This program requires one argument.");
        print("The argument specifies the number of");
        print("digits precision for the second number");
        print("printed.");
        exit(0);
    }

    // Invoke print( double dvalue ).
    print(d);

    // Invoke print( double dvalue, int prec ).
    print(d, atoi(argv[1]));
}

// Print a string.
int print(string s)
{
    cout << s << endl;
    return cout.good();
}

// Print a double in default precision.
int print(double dvalue)
{
    cout << dvalue << endl;
    return cout.good();
}

//  Print a double in specified precision.
//  Positive numbers for precision indicate how many digits
//  precision after the decimal point to show. Negative
//  numbers for precision indicate where to round the number
//  to the left of the decimal point.
int print(double dvalue, int prec)
{
    // Use table-lookup for rounding/truncation.
    static const double rgPow10[] = {
        10E-7, 10E-6, 10E-5, 10E-4, 10E-3, 10E-2, 10E-1,
        10E0, 10E1,  10E2,  10E3,  10E4, 10E5,  10E6 };
    const int iPowZero = 6;

    // If precision out of range, just print the number.
    if (prec < -6 || prec > 7)
    {
        return print(dvalue);
    }
    // Scale, truncate, then rescale.
    dvalue = floor(dvalue / rgPow10[iPowZero - prec]) *
        rgPow10[iPowZero - prec];
    cout << dvalue << endl;
    return cout.good();
}

重载、重写、隐藏

函数和链接性

和变量一样,函数也有链接性,虽然可选择的分为比变量小。和C语言一样,C++不允许在一个函数中定义另一个函数,因此所有函数的存储持续性都自动为静态的,即在整个程序指向期间都一直存在。

默认情况下,函数的链接性为外部的,既可以在文件间共享。

实际上,可以在函数原型中使用extern来指出函数是在另一个文件中定义的。

还可以使用static将函数的链接性设置为内部的,使之只能在一个文件中使用。这还意味着可以在其他文件中定义同名的函数。和变量一样,在定义静态函数的文件中,静态函数将覆盖外部定义,因此即使在外部定义了同名的函数,该文件仍将使用静态函数。

单定义规则也适用于非内联函数,因此,对于每个非内联函数,程序中只能包含一个定义。而内联函数不受这个规则约束,这允许程序员能够将内联函数定义放在头文件中。这样,包含了头文件的每个文件都有内联函数定义,然而,C++要求同一个函数的所有内联定义都必须相同。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值