函数重载
常量与引用:
const与指针
const与函数
const与类
引用(&)
拷贝构造函数
函数重载
当一些函数实现的是同一类功能,只是部分细节不同(比如打印整型、字符型和浮点型),C++提供了函数重载机制,将这些函数取成相同的名字,从而使程序更容易理解及使用
函数重载是指两个或两个以上的函数具有相同的函数名,但参数类型不一致或参数个数不同。编译器根据实参和形参的类型及个数进行相应的匹配,自动确定调用哪一个函数。 使得重载的函数虽然函数名相同,但功能却不完全相同。函数重载是C++堆C的扩展,包括非成员函数的重载和成员函数重载
非成员函数重载
如上文所述:非成员函数重载是指对用户所编写的那些功能相似或类似,参数个数或类型不同的用户自定义函数采用相同的函数名,从而能提高程序的可读性
#include <iostream>
using namespace std;
int mul(int x, int y)
{
return x*y;
}
double mul(double x, double y)
{
return x*y;
}
int main()
{
int x,y;
double a,b;
cout << "Input x,y:" <<endl;
cin >> x >> y;
cout << "x*y = " << mul(x,y) <<endl;
cout << "Input a,b:" <<endl;
cin >> a >> b;
cout << "a*b = " << mul(a,b) <<endl;
return 0;
}
注意:1)重载函数必须具有不同的参数个数或不同的参数类型,若只是返回值的类型不同或形参名不同是不行的(形参名不同并没有任何用,函数的返回值可以相同也可以不同。
2)不要将不同功能的函数定义为重载函数
3)创建重载函数时,必须让编译器能区分重载函数,当函数有多义性时,编译器就不会编译该程序
成员函数重载
成员函数重载的一个很重要的应用就是重载构造函数。 **创建一个对象时,有时需要带参数,也有可能不需要带参数,或是带的参数的个数不一样。**通过对构造函数进行重载,可以实现定义对象时初始化赋值的多样性
#include <iostream>
using namespace std;
class Complex
{
private:
double real;
double imag;
public:
Complex(double r, double i);
Complex(double r);
Complex();
~Complex(){
cout << "destructor" <<endl;
}
void Print();
Complex add(Complex a);
};
Complex::Complex(double r, double i)
{
real = r;
imag = i;
cout << "Constructor:Two" <<endl;
}
Complex::Complex(double r)
{
real = r;
imag = 0;
cout << "Constructor:One" <<endl;
}
Complex::Complex()
{
real = 0;
imag = 0;
cout << "Constructor:Zero" <<endl;
}
Complex Complex::add(Complex a)
{
Complex temp;
temp.real = this->real + a.real;
temp.imag = this->imag + a.imag;
return temp;
}
void Complex::Print()
{
cout <<real;
if(imag > 0)
cout << "+";
if(imag != 0)
cout << imag << "i" <<endl;
}
int main()
{
Complex com1(1.1, 2.2),com2(3.3, 4.4),com3(4.4),total;
total = com1.add(com2);
total.Print();
total = com1.add(com3);
total.Print();
return 0;
}
函数的默认参数
在C++中,提供了默认参数的做法,也就是允许在函数的声明或定义时给一个或多个参数指定默认值。这样在函数调用时,如果不给出实际参数,则可以按指定的默认值进行工作。
如:Complex(double r = 0, double i = 0) 当进行调用时,编译器会按从左到右顺序将实参与形参结合,若未指定足够的实参,则编译器按顺序用函数原型中的默认值来补足所缺少的实参。
Complex(3.5, 9.6); // r = 3.5 i = 9.6
Complex(3.5); // r = 3.5 i = 0
Complex(); // r = 0 i = 0
(1)当函数既有原型声明又有定义时,默认参数只能在原型声明中指定,而不能在函数定义中指定。例如:
Complex (double r = 0, double i = 0);
Complex (double r = 0, double i = 0) //错误情况
{
...
}
(2)在函数原型中,所有取默认值的参数都必须出现在不取默认值的参数的右边。在函数调用时,若某个参数省略,则其后的参数皆应该省略而采用默认值。不允许某个参数省略后,在给其后的参数指定参数值
内联函数
在程序设计中,效率是一个重要的指标。在C中,提高效率的一个方法是使用宏,宏可以不用函数调用,但看起来像函数调用。
宏的实现使用预处理器。预处理器直接用宏代码代替宏调用,因此就不需要函数调用所需的保存调用时的现场状态保存调用时的现场状态和返回地址、就仅从参数传递等时间花费。
详细C中宏与内联可参考https://blog.csdn.net/imgosty/article/details/81901183
然而,C++的预处理器不允许存取私有数据,这意味着预处理器宏在用作成员函数时变得非常无用,为了既保证预处理器宏的效率,又要安全性,且能像一般成员函数一样可以在类里访问自如,因此C++引入了内联函数。
内联函数是一个函数,它与一般函数的区别是在使用时可以像宏一样展开,所以没有函数调用的开销。通过内联函数,它把函数体的代码直接插入到调用处,将调用函数的方式改为顺序执行直接插入的程序代码,减少了程序的执行时间,但也增加了代码的实际长度。
因此使用内联函数可以提高系统的执行效率,但在内联函数体中,不能含有复杂的结构控制语句,如switch和while语句等。内联函数实际上是一种空间换时间的方案,因此其缺点是增大了系统空间方面的开销。
在类内给出函数体定义的成员函数被默认为内联函数??
——答:在类中定义的成员函数全部默认为内联函数。可以显示加上 inline 标识符,或者不加。在类中声明的成员函数,如果没加inline,则在类外定义该成员函数时加了inline,该成员函数也为内联函数。
1.内联函数的定义格式:
inline 返回值类型 函数名(形参表)
{
}
注意:递归调用的函数不能定义为内联函数
常量与引用
const的好处是它允许指定一种语意上的约束:某种对象不能被修改,而由编译器来实施这种约束。
声明格式:const 类型名 对象名;
如:const int MAX = 100;
可以在编译器直到的任何地方使用MAX
注意:1)尽量把const定义放进头文件里,由此通过包含头文件,把const定义放在一个需要的地方,并由编译器分配给它一个编译单元。C++中const为内部连接,即由const定义的常量仅在被定义的文件才能看到,而不会被其他文件看到,除非使用extern。一般情况下,编译器不为const分配空间,而extern强制分配空间。
2)当定义一个const常量时,必须初始化,除非用extern做了清楚的说明
extern const int bufsize;
常量的使用一可以消除不安全因素,二消除存储和读操作,使代码的执行效率更高
3)一个例子👇
const int Datalist[] = {5,8,11,14};
Struct Mystruct {int i; int j;}
const struct Mystruct sList[] = {{1,2},{3,4}};
char cList[Datalist[1]]; //错误
float fList[sList[0].i]; //错误
错误的原因是因为,在编译时编译器必须为数组分配固定大小的内存空间。而使用const修饰的数组意味着“不能被改变”的一块存储区,其值在编译期间不能被使用。
一、 const与指针
const与指针的结合使用,有两种情况:
一是const修饰指针,即修饰存储在指针里的地址;
二是修饰指针指向的对象
采用“靠近”原则,即const离哪个进就是修饰哪个。
1,指向常量的指针:const int *p
,p是一个指针,是一个指向const int 的指针,即p指向一个整型常量,这个常量是const的不能被改变,但是指针p是可以改变的
2,常量指针:类型名 *const 指针名
int i = 4;
int *const q = &i;
const修饰的是指针q,所以q是一个常指针,一个指向int类型的变量i的const指针,q必须有一个初始值,它只能指向这个初始值对象i,不能“被改变”去指向其他对象,但所指向的对象的值是可以改变的
3,可以使用一个常指针指向一个变量,也可以把非const对象变为const对象
int i = 4;
int *const p = &i;//p是常指针,p只能指向这个地址
const int *const p = &i;//把非const对象地址赋给const对象地址??
★也可以用指向字符的指针来指向字符串
char *p = "hello";
此处p为非const指针,指向非const数据,(虽然”hello”为常量),但编译器把它当成非常量来处理,指针指向它在内存中的首地址。虽然没有语法错误,但是不建议这样使用。如果想用指针指向字符串常量,可以这样:
const char *q = “hello!”; //非const指针,const数据
const char *const p = “hello!”; //const指针,const数据
const 与函数
定义格式: 返回值类型 函数名称(const 类型 参数名,……)
void f(const int j)
{
j++;//错误
}
void f(const int *p)
{
(*p)++;//错误
}
参数j在函数f()中不能被改变。
对于传递地址类型的参数,而又不想在函数中改变参数值时,要用const修饰
const类型返回值
可以用const修饰符修饰函数的返回值,即函数返回一个常量,此常量既可以赋给常量,也可以赋给变量。
int res()
{
return 5;
}
const int conres()
{
return 5;
}
int main()
{
int j = res();
j++;
const int i = res(); //正确,函数返回值赋值给常量
int k = conres(); //正确,把常量的值赋给变量
k--; //正确,变量变化
const int f = conres(); //正确,常量赋值给常量
return 0;
}
分析一个例子(结合构造函数)
#include <iostream>
using namespace std;
class Tcons
{
int iData;
public:
Tcons(int i = 0):iData(i)
{
cout<<"构造函数"<<i<<endl;
}
void Seti(int i)
{
iData = i;
cout << "iData:" << iData <<endl;
}
};
Tcons test1()//返回普通对象
{
return Tcons();
}
const Tcons test2()//返回常对象
{
return Tcons();
}
int main()
{
test1() = Tcons(10);
//正确,test1函数返回一个Tcons对象,并把对象Tcons(10)的值赋给它
test1().Seti(20);
test2();
//正确,调用test1(),得到一个返回对象,并调用此对象的成员函数
//test2() = Tcons(10); //错误,常对象不能被修改
//test2().Seti(20); //错误,常对象内容不能被修改
return 0;
}
构造函数:
Tcons(int i = 0):iData(i)
{
cout<<"构造函数"<<i<<endl;
}
调用函数:
test1() = Tcons(10);
test1().Seti(20);
test2();
return 0;
返回结果:
先执行第一个语句右边初始化语句形参i = 20,右边Test()又调用默认的i = 0;所以第一个语句调用了两次构造函数,第二句也是调用构造函数的默认参数,再改值;test2也调用默认值构造函数
const在传递地址中的应用
在const的实参与形参结合时的传递地址的过程中,对于在被调用的函数中不需要修改的指针或对象,用const修饰
#include <iostream>
using namespace std;
void test(int *p){}
void testpointer(const int *p)
{
//(*p)++; //错误,不允许修改常量内容
int i = *p; //正确,常量赋值给变量
//int *q = p; //错误,不能把一个指向常量的指针赋值给一个指向非常量的指针
}
const char *teststring()
{
return "hello!";
}
const int *const testint()
{
static int i = 100;
return &i;
}
int main()
{
int m = 0;
int *im = &m;
const int *cim = &m; //正确,可以把非常量的地址赋给一个指向常量的指针
test(im);
//test(cim); //错误,不能把指向常对象的指针赋值给指向非常对象的指针
testpointer(im);
testpointer(cim);
//char *p = teststring();//错误,不能把指向常对象的指针赋值给指向非>常对象的指针
const char *q = teststring();
cout << *q <<endl;
//int *ip = testint(); //错误,不能把指向常对象的指针赋值给指向非>常对象的指针
const int *const ipm = testint();
cout << *ipm <<endl;
const int *iqm = testint();
cout << *iqm <<endl;
//*testint() = 10; //错误,因为testint()返回值指向常量的指针,其内容不能被修改
return 0;
}
函数teststring()返回值为地址,表明编译器为字符串常量分配的地址。此时const修饰显得很重要;否则,如果允许执行语句char *p = teststring();,将会导致通过指针p来修改“常量”的内容而发生错误。
而函数testint()返回的指针不仅是常量,而且指向的空间为静态的,函数不随着调用的结束而释放指针所指向的空间,调用结束后仍然有效。需要注意的是,函数testint()的返回值类型为const intconst,它可以赋值给const int const类型的,也可以赋值给const int*类型的(编译器不报错,因为返回值是拷贝方式),*第二个const的含义仅当返回量出现在赋值号左边时(testint()=10),const才显示它的含义,编译器才会报错。
const与类
const在类里有两种应用:一时在类里建立类内局部常量,可用在常量表达式中,常量表达式在编译期间内被求值;二是const和成员函数的结合使用
类内const局部变量
在一个类里使用const修饰的意思是“在这个对象寿命期内,这是一个常量”。然而,对这个常量来说,每个不同的对象可以含一个不同的值。
在类里建立一个const成员时不能赋初值,只能在构造函数里对其赋初值,而且要放在构造函数特殊的地方。★★★因为const必须在创建它的地方被初始化,所以在构造函数的主体里,const成员必须已被初始化。
例子👇
class conClass
{
const int NUM;//不能赋初值,在构造函数中已经被初始化
public:
conClass();
};
conClass::conClsaa():NUM(100){};
★★★构造函数定义: 类::类名():私有成员(要赋的值){}
conClass::conClsaa():NUM(100){};
??常用的一个场合就是在类内声明一个常量,用这个常量来定义数组的大小,从而把数组的大小隐藏在类里??why doing this?
错误示例🙅❌
class conClass
{
const int NUM = 100;
int iData[NUM];
public:
conClass();
};
因为在类中进行存储空间分配,编译器不能知道const的内容是什么,所以不能把它用作编译期间的常量。(const常量必须初始化,常量表达式在编译期间就被求值)
====》引出静态常量
为了提高效率,保证所有的类对象最多只有一份拷贝值,通常需要声明为静态的
class Student
{
static const int NUM = 30;
int iScorelist[NUM];
...
};
程序中的NUM,不是定义,而是一个声明,所以在类外还需要加上定义:
const int Student::NUM;
老版本的编辑器不会接受上面的语法,因为它认为类的静态成员在声明时定义初始值是非法的,类内只允许初始化整数类型(如int、bool、char等)
常对象与常成员函数
像声明一个普通的常量一样,可以声明一个复杂的对象为常量
const int i = 10;
const conClass cTest(10);//这是一个对象cTest
因为声明cTest为const类型,所以必须要保证在对象cTest的整个生命周期内不能被改变。对于公有数据很容易做到,然而对于私有数据,如何保证每个成员函数的调用也不改变?需要声明成员函数为const类型,等同于告诉编译器此类的一个const对象可以调用这个成员函数,而const对象调用非const成员函数则不行。
👇
const成员函数定义格式:
class 类名
{
...
返回值类型 成员函数名称(参数列表)const;
...
};
如果在函数的前面加上const,表明函数返回值为const,为了防止混淆,把const放在函数的后面。在一个const成员函数里,试图改变任何数据成员或调用非const成员函数,编译器都将报错。
#include <iostream>
#include <string.h>
using namespace std;
class Student
{
int no;
char name[20];
public:
Student();
int Getno()const;
const char* Getname();
void Print()const;
};
Student::Student()
{
no = 1;
strcpy(name, "wang");
}
int Student::Getno()const
{
return no;
}
const char* Student::Getname()
{
return name;
}
void Student::Print()const
{
cout << "No:" << no << " Name:" << name <<endl;
}
int main()
{
Student s1;
s1.Getno();
s1.Getname();
const Student s2;
s2.Getno();
//s2.Getname(); //错误,常对象调用非const成员函数
s2.Print();
return 0;
}
如果真的想改变常对象的某些数据成员怎么办?
两种方法:
1,强制转换
2,使用mutable
1, 强制转换👇 (*Test)this->j = 5;
#include <iostream>
using namespace std;
class Test
{
int i,j;
public:
Test():i(0),j(0){}
void f()const;
};
void Test::f()const
{
//i = 1; //错误,在常成员函数中修改类成员
((Test*)this)->j = 5;//强制转换
cout << "I:" << i << " J:" << j <<endl;
}
int main()
{
const Test t;
t.f();
return 0;
}
2,mutable,把可能要改变的值定义的时候就定义上mutable
#include <iostream>
using namespace std;
class Test
{
int i;
mutable int j;
public:
Test():i(0),j(0){}
void f()const;
};
void Test::f()const
{
//i = 1; //错误,在常成员函数中修改类成员
j = 5;
cout << "I:" << i << " J:" << j <<endl;
}
int main()
{
const Test t;
t.f();
return 0;
}
引用(&)
在C++中,当函数参数采用传值方式传送时,除非明确指定,否则函数的形参总是通过对“实参的拷贝”来初始化的,函数的调用者得到的也是函数返回值的拷贝。传值方式采用拷贝方式,使得运行效率低下。
虽然通过指针传递地址的方式提高了运行效率,但是相比引用而言,代码不够简洁明了。
引用是C++的一大特点,是支持C++运算符重载的语法基础。,也为函数参数的传入与传出提供了便利。如果不想改变参数,则可通过常量引用const传递。位拷贝是拷贝地址,而值拷贝是拷贝内容。
引用的概念
引用是某个变量或对象的别名,
引用格式:类型名& 引用名 = 被应用的对象名称
访问引用时,实际访问的就是被引用的变量或对象的存储单元
引用就像一个自动能被编译器逆向引用的常量型指针。常用于修饰函数的参数和函数的返回值,但也可以独立使用。
1)当引用被创建时,它必须被初始化(指针可以在任何时候被初始化
2)没有NULL引用。必须确保引用是和一个合法的存储单元关联。
3)一旦一个引用被初始化为指向一个对象,它就不能被改变为对另一个对象的引用(指针可以在任何时候指向另一个对象)
4)注意:不能建立引用数组、不能建立引用的引用
引用与指针
引用与指针有着本质的区别,指针通过变量的地址来间接访问变量,而引用通过变量的别名来直接访问变量。
引用与函数
采用引用的主要用途之一就是做函数的参数使用
当函数的返回值为引用方式时,需要特别注意的是,不要返回一个不存在的或已经销毁的变量的引用
int& tcite2()
{
int m = 2;
//return m; //错误,调用完函数tcite2()后,临时对象m将被释放,返回值为一个空引用
static int x = 5;
return x;
}
int* tpointer(int *p)
{
(*p)++;
return p;
}
int& tcite(int &c)
{
c++;
return c;
}
int main()
{
int i;
tpointer(&i);
tcite(i);
return 0;
}
对常量引用的例子
void t1(int &){}
void t2(const int&){}
int main()
{
//t1(1); //错误,在函数t1()中,可以修改参数内容,而1为常量
t2(1); //正确,在函数t2()中,参数声明为常量
}
拷贝构造函数★★
拷贝构造函数是一种特殊的构造函数,**其形参是本类的对象的引用,**其作用是使用一个已经存在的对象,去初始化一个新的同类对象。
在以下三种情况下会被调用:
①当用一个已经存在的对象,去初始化该类的另一个对象时
②如果函数的形参是类对象,调用函数进行形参和实参结合时
③如果函数的返回值是类对象,函数调用完成返回时
根据情况去定义拷贝构造函数,以实现同类对象之间数据成员的传递。如果没有定义类的拷贝构造函数,系统会在必要时生成一个隐含的拷贝构造函数。 这个隐含的拷贝构造函数的功能是,把初始值对象的每个数据成员值都复制到新建的对象中
#include <iostream>
using namespace std;
class Point
{
public:
Point(int xx = 0, int yy = 0)
{
x = xx;
y = yy;
}
Point(Point &p);
int getx()
{
return x;
}
int gety()
{
return y;
}
private:
int x, y;
};
Point::Point(Point &p)
{
x = p.x;
y = p.y;
cout << "Calling the copy constructor"<<endl;
}
void fun1(Point p)
{
cout << p.getx() <<endl;
}
Point fun2()
{
Point a(1,2);
return a;
}
int main()
{
Point a(4,5); //第一个对象a
Point b = a; //情况1,用a初始化b
cout << b.getx() <<endl;
fun1(b); //情况2,对象b作为fun1的实参
b = fun2(); //情况3,函数返回值是类对象
cout << b.getx() <<endl;
return 0;
}
第二行用a初始化b调用拷贝构造函数,calling
下一行返回4,
fun1(b)
函数的形参是类对象,调用拷贝构造函数calling
并返回4
b = fun2()
函数返回值是类对象,在函数调用完成时调用拷贝构造函数calling
下一行返回1
统计类声明对象个数
#include <iostream>
using namespace std;
class Student
{
private:
static int number;
public:
Student()
{
number++;
show("Student");
}
~Student()
{
number--;
show("Student");
}
static void show(const char* str = NULL)//指向常量的指针
{
if(str)
{
cout << str << ":";
}
cout << "number=" << number <<endl;
}
};
int Student::number = 0;//静态数据成员赋值
Student f(Student x)
{
x.show("x inside f()");
return x;
}
int main()
{
Student h1;
Student h2 = f(h1);
Student::show("after call f()");
return 0;
}
第一个对象h1,调用构造函数,调用show函数,输出
Student:number=1
换行
第二行对象为形参,调用拷贝构造函数,无对应的拷贝构造就调用默认的,先调用show函数,输出x inside f():number=1
问题:调用默认构造函数时哪里?
看答案的话是在跑完函数内之后,是“把初始值对象的每个数据成员值都复制到新建的对象中”的意思……?把number也初始化了?
调用构造函数,输出students:number=0
?
下一行输出字符串:number = 0
还是等于0?
然后调用析构函数,两个对象调用两次,输出分别等于-1,-2
原来是这样:当局部对象在函数f()调用结束时,析构函数被调用,从而number减少就变成0了,然后把初始值对象的每个数据成员值都复制到新建的对象中,所以number是等于0的,
做完下一题后回上来,发现f()函数返回一个类对象,所以还要再次调用构造函数,但是第三行还是因为调用析构函数的原因
改👇增加拷贝构造函数后
#include <iostream>
using namespace std;
class Student
{
private:
static int number;
public:
Student()
{
number++;
show("Student");
}
Student(const Student&)
{
number++;
show("Student");
}
~Student()
{
number--;
show("Student");
}
static void show(const char* str = NULL)//指向常量的指针
{
if(str)
{
cout << str << ":";
}
cout << "number=" << number <<endl;
}
};
int Student::number = 0;//静态数据成员赋值
Student f(Student x)
{
x.show("x inside f()");
return x;
}
int main()
{
Student h1;
Student h2 = f(h1);
Student::show("after call f()");
return 0;
}
1,构造函数,输出第一行 1
2,拷贝构造函数,输出第二行 2,调用f(),输出第三行2,因为返回的也是雷对象,所以在函数调用之后还要调用拷贝构造函数,返回下两行,问题还是有:为什么一个3一个2:因为函数结束时再次调用拷贝构造函数,然后就调用了析构函数吗? 断点看看
对,调用析构函数了
3,调用show函数,返回after行
4,两个析构函数
拷贝构造函数时要注意:
1,并不是所有的类声明中都需要拷贝构造函数,仅仅当准备用传值的方式传递类对象时,才需要复制构造函数
2,为了防止一个对象不被通过传值方式传递,需要声明一个私有拷贝构造函数。因为拷贝构造函数设置为私有,已显示的声明接管了这项工作,所以编译器不再创建默认的拷贝构造函数
#include <iostream>
#include <iomanip>
using namespace std;
class Point
{
private:
static int number;
int x,y;
Point(const Point& p);
public:
Point(int xx = 0, int yy = 0)
{
x = xx;
y = yy;
number++;
show("normal construction");
}
~Point()
{
number--;
show("~Point");
}
void show(const char* p = NULL);
};
int Point::number = 0;
Point::Point(const Point &p)
{
x = p.x;
y = p.y;
number++;
show("copy construction");
}
void Point::show(const char* p)
{
if(p)
cout << p <<":";
cout << number <<endl;
}
void fun1(Point p)
{
p.show("inside fun1()");
}
int main()
{
Point A(1,2);
//Point B(A); //错误,拷贝构造函数为私有,不能被调用
//fun1(A); //错误,同上
return 0;
}