【c++笔记:深入函数】

函数重载

  • 函数重载的定义:重载函数要求同名函数在参数个数或者参数类型上不同,否则,无法实现重载。
  • 除析构函数以外的所有函数都可以被重载。

普通函数重载

#include <iostream>
using namespace std;
void func(int);			//声明一个无返回值并且带有一个整型参数的func函数
void func(float);		//声明一个无返回值并且带有一个浮点型参数的func函数

void main()
{
	int a=1; 				
	float c=1.5; 		
		
	func(a);					
	func(c); 	
}
void func(int a){cout<<"a的平方为:"<<a*a<<endl;}  	
void func(float c){cout<<"c的平方为:"<<c*c<<endl;}

成员函数重载

#include <iostream>
using namespace std;
class cube		//声明一个cube类
{
	public:
			void sum()		//定义一个没有返回值并且不带参数的成员函数sum
			{
				cout<<"i的立方:"<<i*i*i<<"\t"<<"j的立方:"<<j*j*j<<endl;
				//该函数的功能是输出i和j的立方
			}
			void sum(int x,int y)  //定义一个没有返回值并且带两个整型参数的成员函数sum
			{				
				i=x;j=y;
				cout<<"i:"<<i<<"\t\t"<<"j:"<<j<<endl;
			}		//该函数的作用是接收两个整型参数并且将它们赋给私有成员i和j,然后输出i和j
	private:
			int i; 			//私有成员变量i
			int j;			//私有成员变量j
};
void main()
{
	cube a; 			//声明一个cube类的对象a
	a.sum(2,3);		    //用该对象调用带两个整型参数的sum成员函数
	a.sum();			//用该对象调用不带参数的sum成员函数
}

函数的默认参数与重载

  • 普通函数和类的成员函数都可以设置默认参数

默认参数函数的几点注意事项

1.有默认参数的形参必须列在形参列表的最右,即默认参数值得右边不能存在无默认值的参数,因为实参与形参结合时的顺序是从左至右。如:

>  int add(int x,  int y = 0,  int z = 2)    //正确  
>  int add(int x=0,  int y,  int z = 2)      //错误    
>  int add(int x= 0,  int y = 2,  int z )    //错误

2.带有默认参数的函数与重载函数的几点注意,下面是示例代码:

#include <iostream>
using namespace std;

// func
void func(int=0,int=0);

//func_1
//void func_1();
//void func_1(int i);
void func_1(int i, int t = 0);

//func_2
void func_2();
void func_2(int i, int t = 0);

void main()
{
  func();			//调用带默认参数的函数func
  func(3,5);		//调用func函数的同时给它的两个参数赋了3和5两个值
}
void func(int x,int y){cout<<"x:"<<x<<"\t"<<"y:"<<y<<endl;}
......

特别注意

  • 例子中func函数有两种:func()、func(3,5),但是它并没有实现重载,只是参数数值的变化 ,分别对应func(0, 0) 和 func(3,5)。
  • 任意去掉func_1中的一个函数注释,c++编译器都会报两义性错误,因为c++编译器无法识别应该调用哪个函数。
  • 带默认参数的函数在不影响当前作用域下的其他同名函数的情况下,可被视为重载函数(有些文章注明 “函数不能即为重载函数,又带有默认参数”,这种说法不准确,func_2就是重载函数,同时带有默认参数

构造函数的重载

重载的构造函数:自定义构造函数,复制构造函数,类型转换构造函数等

成员变量的赋值与初始化

#include <iostream>
using namespace std;
class rectangle
{
public:
	//初始化
  	 rectangle():length(3),width(5){cout<<"长方形b的面积为:" <<length*width<<endl;}
  	 //赋值
 	 rectangle(int l, int w){length = l; width = w;}
     int area(){return length*width;}
private:
 	 int length;
 	 int width;
};
void main()
{
	//初始化
 	 rectangle b;
 	 //赋值
 	 rectangle c(3,5);
}
  • 常量和引用只能被初始化,不能赋值。这就是初始化数据存在的原因
//const成员或者引用成员都是不可赋值的,因此只能进行初始化。
#include <iostream>
using namespace std;
class A
{
public:
  //A(int a,int b){num=a;total=b;} //错,不能进行赋值
	A(int a,int b):num(a),total(b){}
private:
  const int num;
  int &total;
};
int main()
{
  A a(3,4);
  return 0;
}

成员变量的初始化与构造函数

  • 主要讲述成员变量的初始化顺序
//成员变量的初始化顺序的例程如下:
#include <iostream>
using namespace std;
class demo 
{
public:
    demo(){x=0;cout<<"demo的默认构造函数!"<<x<<endl;}
    demo(int i){x=i;cout<<"demo的带一个参数的构造函数!"<<x<<endl;}
    ~demo(){cout<<"demo的默认析构函数!"<<x<<endl;}
    int get(){return x;}
    void set(int i){x=i;}
private:
    int x;
};
class rectangle
{
public:
  rectangle(){x=1000;cout<<"rectangle的默认构造函数!"<<x<<endl;}
  rectangle(int i,int j,int k): x(i),width(j),length(k)
{
    cout<<"rectangle的带三个参数的构造函数!"<<"长方形b的面积为:" << length.get() 
             *width.get()<<endl;
}
~rectangle(){cout<<"rectangle的默认析构函数!"<<x<<endl;}
int area(){return length.get()*width.get();}
private:
  demo length;
  demo width;
int x;
};
int main()
{
  rectangle a;
  return 0;
}
输出:
demo的带一个参数的构造函数!300
demo的带一个参数的构造函数!200
rectangle的带带三个参数的构造函数!长方形b的面积:60000
rectangle的默认析构函数!100
demo的默认析构函数!200
demo的默认析构函数!300

复制构造函数

概述

  • 也叫拷贝构造函数,是构造函数的一种,通过已经存在的对象来构造一个相同的新对象。
  • 如果未自定义复制构造函数,c++编译器会创建一个默认复制构造函数来实现从源对象到目标对象逐个字节的复制(浅层复制,后面会介绍深层复制)。

参数

1.复制构造的参数必须是引用,引用有两点好处,一个是不用创建零时对象造成的内存损耗,另一个,也是最主要的防止无限递归(如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构 造函数。因此拷贝构造函数的参数必须是一个引用)

2.复制构造函数的参数最好加上const,这是一个很好的习惯,防止自定义拷贝函数时,内部对引用的对象进行了修改的误操作。(复制构造函数的参数可以是 const 引用,也可以是非 const 引用。 一般使用前者,这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。一个类中写两个复制构造函数,一个的参数是 const 引用,另一个的参数是非 const 引用,也是可以的。)

//调用默认复制构造函数
#include<iostream >
using namespace std;
class Complex
{
public:
    double real, imag;
    Complex(double r, double i) {
        real= r; imag = i;
    }
};
int main(){
    Complex cl(1, 2);
    Complex c2 (cl);  //用复制构造函数初始化c2
    cout<<c2.real<<","<<c2.imag;  //输出 1,2
    return 0;
}
// 自定义复制构造函数
#include<iostream>
using namespace std;
class Complex{
public:
    double real, imag;
    Complex(double r,double i){
        real = r; imag = i;
    }
    Complex(const Complex & c){
        real = c.real; imag = c.imag;
        cout<<"Copy Constructor called"<<endl ;
    }
};

int main(){
    Complex cl(1, 2);
    Complex c2 (cl);  //调用复制构造函数
    cout<<c2.real<<","<<c2.imag;
    return 0;
}
程序的输出结果是:
Copy Constructor called
1,2

复制构造函数被调用的三种情况

  1. 当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用。例如,下面的两条语句都会引发复制构造函数的调用,用以初始化 c2。
    Complex c2(c1);
    Complex c2 = c1;
    这两条语句是等价的。
    注意:第二条语句是初始化语句,不是赋值语句。赋值语句的等号左边是一个早已有定义的变量,赋值语句不会引发复制构造函数的调用。例如:
    纯文本复制
    Complex c1, c2; c1 = c2 ;
    c1=c2;
    这条语句不会引发复制构造函数的调用,因为 c1 早已生成,已经初始化过了。

2.形参是一个对象,并且是按值传递。

#include<iostream>
using namespace std;
class A{
public:
    A(){};
    A(A & a){
        cout<<"Copy constructor called"<<endl;
    }
};

void Func(A a){ }

int main(){
    A a;
    Func(a);
    return 0;
}
程序的输出结果为:
Copy constructor called

这种以对象的传参方式会出现几个问题

  • 函数的形参的值不等于函数调用时对应的实参,因为复制构造函数什么都没做,所以,自定义拷贝函数时,一定要特别注意拷贝数据。
  • 拷贝零时对象会增加内存和时间的开销,用引用能很好的解决这个问题。
  • 如果是自定义拷贝构造函数,且参数是引用方式,一定要给参数加上const防止对象数据被误操作
    例如:
    void Function(const Complex & c)
    {

    }
    这样,Function 函数中出现任何有可能导致 c 的值被修改的语句,都会引发编译错误。

3.如果函数的返冋值是类 A 的对象,则函数返冋时,类 A 的复制构造函数被调用。换言之,作为函数返回值的对象是用复制构造函数初始化 的,而调用复制构造函数时的实参,就是 return 语句所返回的对象。例如下面的程序:

#include<iostream>
using namespace std;
class A {
public:
    int v;
    A(int n) { v = n; };
    A(const A & a) {
        v = a.v;
        cout << "Copy constructor called" << endl;
    }
};

A Func() {
    A a(4);
    return a;
}

int main() {
    cout << Func().v << endl;
    return 0;
}
程序的输出结果是:
Copy constructor called
4

需要说明的是,有些编译器出于程序执行效率的考虑,编译的时候进行了优化,函数返回值对象就不用复制构造函数初始化了,这并不符合 C++ 的标准。上面的程序,用 Visual Studio 2010 编译后的输出结果如上所述,但是在 Dev C++ 4.9 中不会调用复制构造函数。把第 14 行的 a 变成全局变量,才会调用复制构造函数。对这一点,读者不必深究。

浅层与深层复制构造函数

  • 如果对象的内部数据存在指针类型,在用复制构造函数时要格外小心。
浅层复制
#include <iostream>
using namespace std;
class A 
{
public:
    A(){x=new int;*x=5;}     		//创建一个对象的同时将成员指针指向的变量保存到 新空间中
    ~A(){delete x;x = NULL;} 		//析构对象的同时删除成员指针指向的内存空间,为了稳妥起见将指针赋为空
    A(const A &a)
    {
        cout << "复制构造函数执行...\n" <<endl;
        x = a.x;        //浅层复制,复制了指针x指向的内存地址,导致复制的新对象和老对象共享同一块内存数据    		
    }
    int print()const{return *x;}
    void set(int i){*x=i;}
private:
    int *x;
};
int main() 
{
    A *a = new A();
    cout<<"a:"<<a->print()<<endl;
    A b=(*a);
    a->set(32);	
    cout<<"b:"<<b.print()<<endl;
    b.set(99);
    cout<<"a:"<<a->print()<<endl;  
    delete a;
    return 0;
}
a:5
复制构造函数执行...
b:32
a:99
深层复制
//深层复制的例程如下:
#include <iostream>
using namespace std;
class A 
{
public:
    A(){x=new int;*x=5;}     		//创建一个对象的同时将成员指针指向的变量保存到新空间中
    ~A(){delete x;x = NULL;} 		//析构对象的同时删除成员指针指向的内存空间,
                                              // 为了稳妥起见将指针赋为空
    A(const A &a)
    {
        cout << "复制构造函数执行...\n" <<endl;
        x=new int;               	//复制构造对象的同时将成员指针指向的变量保存到新空间中
        *x = *(a.x);            		//读取旧对象的成员指针x指向的空间处的数据并
                                              // 赋给新对象的成员指针x所指向的内存区域
    }
    int print()const{return *x;}
    void set(int i){*x=i;}
private:
    int *x;
};
int main() 
{
    A *a = new A();
    cout<<"a:"<<a->print()<<endl;
    A b=(*a);
    a->set(32);	
    cout<<"b:"<<b.print()<<endl;
    b.set(99);
    cout<<"a:"<<a->print()<<endl;  
    delete a;
    return 0;
}

a:5
复制构造函数执行...
b:5
a:32

类型转换构造函数

类型转换

在 C/C++ 中,不同的数据类型之间可以相互转换。无需用户指明如何转换的称为自动类型转换(隐式类型转换),需要用户显式地指明如何转换的称为强制类型转换。

隐式类型转换
//隐式类型转换示例:
int a = 6;
a = 7.5 + a;
强制类型转换
//强制类型转换示例:
int n = 100;
int *p1 = &n;
float *p2 = (float*)p1;

p1 是int *类型,它指向的内存里面保存的是整数,p2 是float *类型,将 p1 赋值给 p2 后,p2 也指向了这块内存,并把这块内存中的数据作为小数处理。我们知道,整数和小数的存储格式大相径庭,将整数作为小数处理非常荒诞,可能会引发莫名其妙的错误,所以编译器默认不允许将 p1 赋值给 p2。但是,使用强制类型转换后,编译器就认为我们知道这种风险的存在,并进行了适当的权衡,所以最终还是允许了这种行为。

注意事项

不管是自动类型转换还是强制类型转换,前提必须是编译器知道如何转换,例如,将小数转换为整数会抹掉小数点后面的数字,将int *转换为float *只是简单地复制指针的值,这些规则都是编译器内置的,我们并没有告诉编译器。

换句话说,如果编译器不知道转换规则就不能转换,使用强制类型也无用,请看下面的例子:

#include <iostream>
using namespace std;
//复数类
class Complex{
public:
    Complex(): m_real(0.0), m_imag(0.0){ }
    Complex(double real, double imag): m_real(real), m_imag(imag){ }
public:
    friend ostream & operator<<(ostream &out, Complex &c);  //友元函数
private:
    double m_real;  //实部
    double m_imag;  //虚部
};
//重载>>运算符
ostream & operator<<(ostream &out, Complex &c){
    out << c.m_real <<" + "<< c.m_imag <<"i";;
    return out;
}
int main(){
    Complex a(10.0, 20.0);
    a = (Complex)25.5;  //错误,转换失败
    return 0;
}

25.5 是实数,a 是复数,将 25.5 赋值给 a 后,我们期望 a 的实部变为 25.5,而虚部为 0。但是,编译器并不知道这个转换规则,这超出了编译器的处理能力,所以转换失败,即使加上强制类型转换也无用。

自定义转换类型

幸运的是,C++ 允许我们自定义类型转换规则,用户可以将其它类型转换为当前类类型,也可以将当前类类型转换为其它类型。这种自定义的类型转换规则只能以类的成员函数的形式出现,换句话说,这种转换规则只适用于类

转换构造函数
  • 如果没有自定义转换构造函数,c++编译器
    会提供一个转换函数(带一个参数),用在将数字当作对象赋值给另一个对象的情况。如:
//编译器自带类型转换示例
#include <iostream>
using namespace std;
class A 
{
public:
    A(int x){i=x;cout<<"构造函数执行!"<<i<<endl;}
    ~A(){cout<<"析构函数执行!"<<i<<endl;}
void get(){cout<<i<<endl;}
private:
    int i;
};
int main()
{

    A a(99);			//调用A(int x)输出"构造函数执行!99"
    a.get();
    a=1000;			//调用A(int x)将1000转换成类A临时对象并赋值给a,输出
                    //         "构造函数执行!1000"
    //接着调用~A()将刚创建的临时对象析构,输出"析构函数执行!1000"
    a.get();			//调用get()输出1000
    
    return 0;
}
运行结果:
构造函数执行!99
99
构造函数执行!1000
析构函数执行!1000
1000
析构函数执行!1000
  • 转换构造函数只能有一个参数。 (类具有根据参数类型自动适配构造函数的特性,但是转换构造函数只能识别一个参数,因此可以这么写:Complex a = 25.5,但是不能:Complex a = 25.5,12.5)
#include <iostream>
using namespace std;
//复数类
class Complex{
public:
    Complex(): m_real(0.0), m_imag(0.0){ }
    Complex(double real, double imag): m_real(real), m_imag(imag){ }
    Complex(double real): m_real(real), m_imag(0.0){ }  //自定义转换构造函数
public:
    friend ostream & operator<<(ostream &out, Complex &c);  //友元函数
private:
    double m_real;  //实部
    double m_imag;  //虚部
};
//重载>>运算符
ostream & operator<<(ostream &out, Complex &c){
    out << c.m_real <<" + "<< c.m_imag <<"i";;
    return out;
}
int main(){
    Complex a(10.0, 20.0);
    cout<<a<<endl;
    a = 25.5;  //调用转换构造函数
    cout<<a<<endl;
    return 0;
}
运行结果:
10 + 20i
25.5 + 0i

a = 25.5 相当于a.Complex(25.5),将赋值的过程转换成了函数调用的过程。

  • 转换构造函数也是构造函数的一种,它除了可以用来将其它类型转换为当前类类型,还可以用来初始化对象,这是构造函数本来的意义。下面创建对象的方式是正确的:
Complex c1(26.4);  //创建具名对象
Complex c2 = 240.3;  //以拷贝的方式初始化对象
Complex(15.9);  //创建匿名对象
c1 = Complex(46.9);  //创建一个匿名对象并将它赋值给 c1

在以拷贝的方式初始化对象时,编译器先调用转换构造函数,将 240.3 转换为 Complex 类型(创建一个 Complex 类的匿名对象),然后再拷贝给 c2。

如果已经对+运算符进行了重载,使之能进行两个 Complex 类对象的相加,那么下面的语句也是正确的:

Complex c1(15.6, 89.9);
Complex c2;
c2 = c1 + 29.6;
cout<<c2<<endl;

在进行加法运算符时,编译器先将 29.6 转换为 Complex 类型(创建一个 Complex 类的匿名对象)再相加。

  • 需要注意的是,为了获得目标类型,编译器会“不择手段”,会综合使用内置的转换规则和用户自定义的转换规则,并且会进行多级类型转换,例如:
    编译器会根据内置规则先将 int 转换为 double,再根据用户自定义规则将 double 转换为 Complex(int --> double --> Complex);
    编译器会根据内置规则先将 char 转换为 int,再将 int 转换为 double,最后根据用户自定义规则将 double 转换为 Complex(char --> int --> double --> Complex)。

从本例看,只要一个类型能转换为 double 类型,就能转换为 Complex 类型。请看下面的例子:

int main(){
    Complex c1 = 100;  //int --> double --> Complex
    cout<<c1<<endl;
    c1 = 'A';  //char --> int --> double --> Complex
    cout<<c1<<endl;
    c1 = true;  //bool --> int --> double --> Complex
    cout<<c1<<endl;
    Complex c2(25.8, 0.7);
    //假设已经重载了+运算符
    c1 = c2 + 'H' + true + 15;  //将char、bool、int都转换为Complex类型再运算
    cout<<c1<<endl;
    return 0;
}
运行结果:
100 + 0i
65 + 0i
1 + 0i
113.8 + 0.7i
  • 对 Complex 类的进一步精简
#include <iostream>
using namespace std;
//复数类
class Complex{
public:
    Complex(double real = 0.0, double imag = 0.0): m_real(real), m_imag(imag){ }
public:
    friend ostream & operator<<(ostream &out, Complex &c);  //友元函数
private:
    double m_real;  //实部
    double m_imag;  //虚部
};
//重载>>运算符
ostream & operator<<(ostream &out, Complex &c){
    out << c.m_real <<" + "<< c.m_imag <<"i";;
    return out;
}
int main(){
    Complex a(10.0, 20.0);  //向构造函数传递 2 个实参,不使用默认参数
    Complex b(89.5);  //向构造函数传递 1 个实参,使用 1 个默认参数
    Complex c;  //不向构造函数传递实参,使用全部默认参数
    a = 25.5;  //调用转换构造函数(向构造函数传递 1 个实参,使用 1 个默认参数)
    return 0;
}

精简后的构造函数包含了两个默认参数,在调用它时可以省略部分或者全部实参,也就是可以向它传递 0 个、1 个、2 个实参。转换构造函数就是包含了一个参数的构造函数,恰好能够和其他两个普通的构造函数“融合”在一起。


参考连接:https://blog.csdn.net/ccc369639963/article/details/122905386

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值