第一章:从C到C++
1. 引用
格式:类型名 & 引用名 = 某变量名;
- 引用的本质是,让 引用变量 和 被引用变量 共同拥有同一个内存空间。
- 引用只能引用变量,不能引用常量和表达式。
- 引用也可以作为函数的返回值,此时可以直接给函数赋值:
#include<iostream>
#include<cstring>
using namespace std;
int n = 4;
int & SetValue() { return n; }
int main()
{
SetValue() = 40;
cout << n;
return 0;
} //输出: 40
- 常引用的用法:
int n; const int & r = n;
int a = 1;
const int &r = a;
r = 11; //错误,此处报错不能给常量赋值!!
cout << a;
2. const关键字
- const 可以用来定义常量,比define更好,因为const定义的时候可以定义变量名
- const可以定义常量指针,
const int * p = &r;
但是常量指针不能用来修改其指向的内容,但是常量指针的指向可以变化。(只读!!)
int n,m;
const int * p = & n;
* p = 5; //编译出错
n = 4; //ok
p = &m; //ok, 常量指针的指向可以变化
- 常量指针不可以给非常量指针赋值!!(只能读取,不能写出)(原因:如果允许的话,通过被赋值的常量指针就可以修改常量指针指向的内容。但实际上我们不希望修改常量指针所指向内存,这样容易造成程序的不安全)。如果非要用非常量指针指向一个常量指针指向的地址,也可通过强制转换来实现。
int a = 1,b = 2;
const int *r = &a;
int *p = &b;
r = p;
p = r; // error!常量指针不可以给非常量指针赋值
p = (int *)r; // 强制转换来实现
- 如果不希望函数执行的过程中修改参数,可以把参数设置为const,以避免其被修改。
void MyPrintf(const char * p)
{
strcpy(p, "this"); //编译出错
printf("%s", p); //ok
}
3. 动态的内存分配
- 格式:
T * P = new T;
T * P = new T[N];
分别动态分配出一片大小为sizeof(T) 或者 sizeof(T)*N 字节的内存空间,并且将该内存空间的起始地址赋值给P。
其中:
T :任意类型名
P :类型为T * 的指针
N :要分配的数组元素的个数,可以是整型表达式 - 用“new”动态分配的内存空间,一定要用“delete”运算符进行释放
- 对分配出的变量空间:delete 指针;
- 对分配出的数组空间:delete [ ] 指针;
4. 内联函数
-
函数调用是有时间开销的。
如果函数本身只有几条语句,执行非常快,而且函数被反复执行很多次,相比之下调用函数所产生的这个开销就会显得比较大。 -
为了减少函数调用的开销,引入了内联函数机制。
编译器处理对内联函数的调用语句时,是将整个函数的代码插入到调用语句处,而不会产生调用函数的语句。 -
在函数前加上“inline” 关键字,就可以定义为内联函数。
-
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
-
以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现复杂的结构控制语句例如while、switch,并且本身直接递归函数(自己内部还调用自己的函数)。那么执行函数体内代码的时间要比函数调用的开销大。
5. 函数重载
- 一个或多个函数,名字相同,然而参数个数或参数类型不相同,这叫做函数的重载。
- 函数重载使得函数命名变得简单。 但要注意,不同的函数重载之间必须没有交集,即一种参数不能同时符合多个加粗样式函数的重载,否则编译器就会直接报二义性的错误。
- class “类”的成员函数也可以重载,也可以有缺省值,使用方法一致。
6. 函数的缺省参数
- 定义函数的时候可以让最右边的连续若干个参数有缺省值,那么调用函数的时候,若相应位置不写参数,参数就是缺省值。
void func(int x1, int x2 = 2, int x3 = 3) { }
func(10); //等效于 func(10,2,3)
func(10, 8); //等效于 func(10,8,3)
func(10, , 8); //不行,只能最右边的连续若干个参数缺省
- 意义:
- 函数参数可缺省的目的在于提高程序的可扩充性。
(即如果某个写好的函数要添加新的参数,而原先那些调用该函数的语句,未必需要使用新增的参数,那么为了避免对原先那些函数调用语句的修改,就可以使用缺省参数。)
第二章:类和对象
1. 使用类的成员变量和成员函数
- 1.对象名.成员名
CRectangle r1,r2;
r1.w = 5;
r2.Init(5,4);
- 2.用法2. 指针->成员名
CRectangle r1,r2;
CRectangle * p1 = & r1;
CRectangle * p2 = & r2;
p1->w = 5;
p2->Init(5,4); //Init作用在p2指向的对象上
- 用法3:引用名.成员名
CRectangle r2;
CRectangle & rr = r2;
rr.w = 5;
rr.Init(5,4); //rr的值变了, r2的值也变
void PrintRectangle(CRectangle & r)
{ cout << r.Area() << ","<< r.Perimeter(); }
CRectangle r3;
r3.Init(5,4);
PrintRectangle(r3);
2. 类成员的可访问范围
- 在类的定义中,用下列访问范围关键字来说明类成员可被访问的范围:
– private: 私有成员,只能在成员函数内访问
– public : 公有成员,可以在任何地方访问
– protected: 保护成员,以后再说
class className {
private:
私有属性和函数
public:
公有属性和函数
protected:
保护属性和函数
};
- 如过某个成员前面没有上述关键字,则缺省地被认为是私有成员。
- 在类的成员函数内部,能够访问:
– 当前对象的全部属性、函数;
– 同类其它对象的全部属性、函数。- 设置私有成员的机制,叫“隐藏”:
“隐藏”的目的是强制对私有成员量的访问一定要通过成员函进行,那么以后成员变量的类型等属性修改后,只需要更改成函数即可。否则,所有直接访问成员变量的语句都需要修改。
3. 构造函数
1. 介绍
1) 构造函数是在对象生成的时候自动调用的,用来对对象进行初始化的函数,它是成员函数的一种,名字和类的名字一样,可以有参数,但是不能有返回值!
class Complex {
private:
double real, imag;
public:
Complex(double r, double i = 0);
};
Complex::Complex(double r, double i)
{
real = r; imag = i;
}
Complex c1; // error, 缺少构造函数的参
数
Complex * pc = new Complex; // error, 没有参数
Complex c1(2); // OK
Complex c1(2, 4), c2(3, 5);
Complex * pc = new Complex(3, 4);
2)构造函数不是用来给类对象分配存储空间,而是在类对象已经占用了存储空间之后,用来对对象进行初始化,比如给成员变量赋初值。
3) 类对象一定是有构造函数的!如果在定义一个类的时候,没有写构造函数,编译器会生成一个默认的无参构造函数,定义的对象初始化的时候会直接调用构造函数,此时构造函数不起作用,只是为了满足语法格式的正确性。 但是,如果在类里面自定义了一个有参数的构造函数,编译器就不会生成默认的无参构造函数,此时在定义对象的时候,会直接调用自定义的有参构造函数进行初始化。
4)一个类可以定义多个构造函数,具体使用哪个构造函数对对象进行初始化,取决于生成对象的语句所提供的参数类型,多个构造函数之间是重载的关系。
- 为什么需要构造函数:
- 构造函数执行必要的初始化工作,有了构造函数,就不必专门再写初始化函数,也不用担心忘记调用初始化函数。
- 有时对象没被初始化就使用,会导致程序出错。
2. 构造函数的使用
- 对象数组中,构造函数的参数在等号右侧给出
例子1:无参构造和有一个参数的构造函数的使用方式
class CSample {
int x;
public:
CSample() {
cout << "Constructor 1 Called" << endl;
}
CSample(int n) {
x = n;
cout << "Constructor 2 Called" << endl;
}
};
int main() {
CSample array1[2];
cout << "step1" << endl;
CSample array2[2] = { 4,5 };
cout << "step2" << endl;
CSample array3[2] = { 3 };
cout << "step3" << endl;
CSample * array4 =
new CSample[2];
delete[]array4;
return 0;
}
输出:
Constructor 1 Called
Constructor 1 Called
step1
Constructor 2 Called
Constructor 2 Called
step2
Constructor 2 Called
Constructor 1 Called
step3
Constructor 1 Called
Constructor 1 Called
例子2:两个参数构造函数的使用方式
class Test {
public:
Test(int n) { } //(1)
Test(int n, int m) { } //(2)
Test() { } //(3)
};
Test array1[3] = { 1, Test(1,2) };
// 三个元素分别用(1),(2),(3)初始化
Test array2[3] = { Test(2,3), Test(1,2) , 1 };
// 三个元素分别用(2),(2),(1)初始化
Test * pArray[3] = { new Test(4), new Test(1,2) };
//两个元素分别用(1),(2) 初始化,只生成了两个对象!!
3. 复制构造函数
形如 X::X( X& ) 或 X::X(const X &) , 二者选一后者能以常量对象作为参数。
如果没有定义复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制功能。如果定义的自己的复制构造函数,则默认的复制构造函数不存在。
class Complex {
public:
double real, imag;
Complex() { }
Complex(const Complex & c) {
real = c.real;
imag = c.imag;
cout << “Copy Constructor called”;
}
};
Complex c1;
Complex c2(c1);//调用自己定义的复制构造函数,输出 Copy Constructor called
复制构造函数起作用的三种情况:
-
当用一个对象去初始化同类的另一个对象时。
Complex c2(c1);
Complex c2 = c1; //初始化语句,非赋值语句
-
如果某函数有一个参数是类 A 的对象,
那么该函数被调用时,类A的复制构造函数将被调用。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class A {
public:
A() { };
A(A & a) { cout << "Copy constructor called" << endl; }
};
void Func(A a1) { }
int main() {
A a2;
Func(a2);
return 0;
}
程序输出结果为: Copy constructor called
注:
void fun(CMyclass obj_ ) {
cout << "fun" << endl;
}
有时候,我们调用函数的参数是类的对象时,调用时生成形参会引发复制构造函数调用,开销比较大。 所以可以考虑 使用引用类型作为参数。
如果希望确保实参的值在函数中不应被改变,那么可以加上const 关键字:
void fun(const CMyclass & obj) {
cout << "fun" << endl;
//函数中任何试图改变 obj值的语句都将是变成非法
}
- 如果函数的返回值是类A的对象时,则函数返回时,A的复制构造函数被调用:
#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 b(4);
return b;
}
int main() {
cout << Func().v << endl;
return 0;
}
注:赋值语句,不调用构造函数,比如 c2 = c1;
4. 类型转换构造函数
解释:
定义转换构造函数的目的是实现类型的自动转换。
- 只有一个参数,而且不是复制构造函数的构造函数,一般
就可以看作是转换构造函数。 - 当需要的时候,编译系统会自动调用转换构造函数,建立
一个无名的 临时对象 (或临时变量)。
class Complex {
public:
double real, imag;
Complex(int i) {//类型转换构造函数
cout << "IntConstructor called" << endl;
real = i; imag = 0;
}
Complex(double r, double i) { real = r; imag = i; }
};
int main()
{
Complex c1(7, 8);
Complex c2 = 12;
c1 = 9; // 9被自动转换成一个临时Complex对象
cout << c1.real << "," << c1.imag << endl;
return 0;
}
class Complex {
public:
double real, imag;
explicit Complex(int i) {//显式类型转换构造函数
cout << "IntConstructor called" << endl;
real = i; imag = 0;
}
Complex(double r, double i) { real = r; imag = i; }
};
int main() {
Complex c1(7, 8);
Complex c2 = Complex(12);
c1 = 9; // error, 9不能被自动转换成一个临时Complex对象
c1 = Complex(9); //ok
cout << c1.real << "," << c1.imag << endl;
return 0;
}
4. 析构函数
解释:
- 名字与类名相同,在前面加‘~’ , 没有参数和返回值,一个类最多只能有一个析构函数。
- 每个对象生命期结束时,对象的析构函数都会被自动调用。析构函数可以理解为,在对象消亡前做善后工作,比如 释放分配的空间 等。
- 但是,对于动态分配的内存,必须要用 delete运算符 才能释放动态分配的内存,此时不会自动调用析构函数。PS:若new一个对象数组,那么用delete释放时应该写 []。否则只delete一个对象(调用一次析构函数)(如delete [ ] XXX):
#include <iostream>
using namespace std;
class CMyclass {
public:
~CMyclass() { cout << "destructor" << endl; }
};
CMyclass obj;
CMyclass fun(CMyclass sobj) { //参数对象消亡也会导致析
//构函数被调用
return sobj; //函数调用返回时生成临时对象返回
}
int main() {
obj = fun(obj); //函数调用的返回值(临时对象)被用过后,该临时对象调用析构函数被
CMyclass * p = new CMyclass[3]; //动态分配数组内存
delete[]p; //析构函数调用3次,把数组里的内存空间全部释放
return 0; //
}
输出:
destructor
destructor
destructor
destructor
destructor
destructor
- 如果定义类时没写析构函数,则编译器生成 缺省析构函数。缺省析构函数什么也不做。
- 如果定义了析构函数,则编译器不生成缺省析构函数。
- 整个程序结束之后,全局变量会自动调用析构函数,顺序是:后生成的全局变量先析构。
5. 构造函数和析构函数的调用
#include <iostream>
using namespace std;
class Demo {
int id;
public:
Demo(int i) {
id = i;
cout << "id=" << id << " constructed" << endl;
}
~Demo() {
cout << "id=" << id << " destructed" << endl;
}
};
Demo d1(1);
void Func()
{
static Demo d2(2);
Demo d3(3);
cout << "func" << endl;
}
int main() {
Demo d4(4);
d4 = 6;
cout << "main" << endl;
{ Demo d5(5);
}
Func();
cout << "main ends" << endl;
return 0;
}
#include <iostream>
using namespace std;
class CMyclass {
public:
CMyclass() {};
CMyclass(CMyclass & c) { cout << "copy constructor" << endl; }
~ CMyclass() { cout << "destructor" << endl; }
};
void fun(CMyclass obj_) { cout << "fun" << endl; }
CMyclass c;
CMyclass Test() {
cout << "test" << endl;
return c;
}
int main() {
CMyclass c1;
fun(c1);
Test();
return 0;
}
输出结果:
copy constructor
fun
destructor //参数消亡
test
copy constructor
destructor // 返回值临时对象消亡
destructor // 局部变量消亡
destructor // 全局变量消亡
#include <iostream>
using namespace std;
class A {
public:
int x;
A(int x_) :x(x_)
{
cout << x << " constructor called" << endl;
}
A(const A & a) { //本例中dev需要此const其他编译器不要
x = 2 + a.x;
cout << "copy called" << endl;
}
~A() { cout << x << " destructor called" << endl; }
};
A f() { A b(10); return b; }
int main() {
A a(1);
a = f();
return 0;
}
Visual Studio 输出结果:
1 constructor called
10 constructor called
copy called
10 destructor called
12 destructor called
12 destructor called
#include <iostream>
using namespace std;
class A {
public:
int v;
A(int n) {
v = n;
cout << "Copy constructor 1" << endl;
};
A(const A & a) {
v = a.v;
cout << "Copy constructor 2" << endl;
}
~A( ) { cout << v <<" "<<"Deconstructor" << endl; }
};
A Func() {
A b(4); return b;
}
int main() {
A tmp = Func();
tmp = 6;
cout << Func().v << endl;
return 0;
}
输出:
Copy constructor 1 //调用函数的参数是类的对象,调用复制构造函数
Copy constructor 2 //函数返回值是类A的对象时,调用A的复制构造函数
4 Deconstructor //函数结束,局部变量析构消亡
Copy constructor 1 //类型转换构造函数,自动把整形对象转换成临时对象,然后把临时对象的值赋给tmp
6 Deconstructor //临时对象在语句执行完之后会消亡析构
Copy constructor 1 //同上
Copy constructor 2 //同上
4 Deconstructor //同上
4 //同上
4 Deconstructor //临时对象在语句执行完之后会消亡析构
6 Deconstructor //局部变量tmp消亡析构
6. this指针
导言:
C++最初是基于C语言的,功能更强大的语言,其优势在于面向对象的编程特点。C++的类对象 class 对标的是C语言中的 struct 。但struct结构中并没有成员函数,我们也无法像class一样,定义一个类对象,然后直接用类对象.成员函数
直接来处理成员变量,所以我们在没有成员函数的struct中,只能定义一个全局函数,来实现结构的成员变量的处理。但是调用全局函数时,函数无法识别结构体内成员变量的地址,所以在传参的时候,需要多一个struct成员变量的地址参数。
#include <iostream>
using namespace std;
struct CCar {
int price;
};
void SetPrice( CCar * a, int p) {
a->price = p;
}
int main() {
struct CCar car;
SetPrice(&car, 20000);
cout << car.price;
return 0;
}
转换成C++之后:
#include <iostream>
using namespace std;
class CCar {
public:
int price;
void SetPrice(int p);
};
void CCar::SetPrice(int p) {
price = p;
}
int main() {
CCar car;
car.SetPrice(20000);
cout << car.price;
return 0;
}
总结:
this指针的作用就是指向成员函数所作用的对象,非静态成员函数中可以直接用this来代表 指向该函数作用的对象的指针。
#include <iostream>
using namespace std;
class Complex {
public:
double real, imag;
void Print() { cout << real << "," << imag; }
Complex(double r, double i) :real(r), imag(i) { }
Complex AddOne() {
this->real++; //等价于 real ++;
this->Print(); //等价于 Print
return *this;
}
};
int main() {
Complex c1(1, 1), c2(0, 0);
c2 = c1.AddOne();
return 0;
}
注意:
- this指针只能处于非静态成员函数的内部,不能用于静态成员函数内部 !!!
- 因为静态成员函数并不具体作用与某个对象 !(比如static函数不属于某个对象)
- 因此,静态成员函数的真实的参数的个数,就是程序中写出的参数个数!(非晶态成员函数需要多一个参数,即指向该函数作用的对象的this指针)
- this指针是类的一个自动生成、自动隐藏的私有成员,它存在于类的非静态成员函数中,指向被调用函数所在的对象。全局仅有一个this指针,当一个对象被创建时,this指针就存放指向对象数据的首地址。
7. 静态成员变量/函数
- 基本概念:
静态成员:在定义前面加了static关键字的成员。 - 普通成员变量每个对象有各自的一份,而静态成员变量一共就一份,为所有对象共享。普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用于某个对象。因此静态成员不需要通过对象就能访问。同时,sizeof( ) 运算符不会计算对象里的静态成员变量。
class CMyclass {
int n;
static int s;
};
则 sizeof(CMyclass) 等于 4
- 如何访问静态成员:
-
类名::成员名
CRectangle::PrintTotal();
-
对象名.成员名
CRectangle r; r.PrintTotal();
-
指针->成员名
CRectangle * p = &r; p->PrintTotal();
-
引用.成员名
CRectangle & ref = r; int n = ref.nTotalNumber;
- 理解:
- 静态成员变量本质上是全局变量,哪怕一个对象都不存在,类的静态成员变量也存在。
- 设置静态成员变量的目的是把和某些紧密相关的全局变量和函数写入类里面,看起来像是一个整体,便于维护和理解。
- 必须在定义类的文件中对静态成员变量进行一次说明或初始化。否则编译能通过,链接不能通过。
比如:
int CRectangle::nTotalNumber = 0;
int CRectangle::nTotalArea = 0;
-
在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数。 (因为非静态成员函数中可能包含非静态成员变量,此处解释不了,该非静态变量属于哪个对象!!)
-
在使用CRectangle类时,有时会调用复制构造函数生成 临时的 隐藏的CRectangle对象
CRectangle::CRectangle(int w_, int h_) {
w = w_;
h = h_;
nTotalNumber++;
nTotalArea += w * h;
}
// 解决办法:为CRectangle类写一个复制构造函数。
CRectangle::CRectangle (CRectangle & r) {
w = r.w; h = r.h;
nTotalNumber++;
nTotalArea += w * h;
}
CRectangle:: ~ CRectangle() {
nTotalNumber--;
nTotalArea -= w * h;
}
void CRectangle::PrintTotal() {
cout << nTotalNumber << "," << nTotalArea << endl;
}
- 调用一个以 CRectangle类对象作为参数 的函数时
- 调用一个以 CRectangle类对象作为返回值 的函数时
临时对象在消亡时会调用析构函数,减少nTotalNumber 和 nTotalArea的值,可是这些临时对象在生成时却没有增加
nTotalNumber 和 nTotalArea的值。
解决办法:为CRectangle类写一个复制构造函数。
8. 成员对象和封闭类
- 有成员对象的类叫 封闭类(enclosing)。
成员对象指的是,某个类的成员变量同时也是其他类的对象,则称之为成员对象。 - 任何生成封闭类对象的语句,都要让编译器明白,对象中的成员对象,是如何初始化的。
具体的做法就是: 通过封闭类的构造函数的初始化列表。
成员对象初始化列表中的参数可以是任意复杂的表达式,可以包括函数,变量,只要表达式中的函数或变量有定义就行。
#include <iostream>
using namespace std;
class CTyre { //轮胎类
private:
int radius; //半径
int width; //宽度
public:
CTyre(int r,int w) :radius(r), width(w) {
cout << "CTyre contructor" <<": "<< "radius:" << radius << " " << "width:" << width << " " << endl; }
CTyre() { cout <<"CTyre contructor" << endl; }
~CTyre() { cout<< "CTyre destructor" << endl; }
};
class CEngine { //引擎类
public:
CEngine() { cout << "CEngine contructor" << endl; }
~CEngine() { cout << "CEngine destructor" << endl; }
};
class CCar { //汽车类
private:
int price;
CEngine engine;
CTyre tyre;
public:
CCar(int a,int r,int w):price(a),tyre(r,w) {
cout << "CCar contructor" << endl; }
CCar() {
cout << "CCar contructor" << endl;
}
~CCar() { cout << "CCar destructor" << endl; }
};
int main() {
CCar car1(50,10,10);
cout << "--------------------------------------------------" << endl;
CCar car2;
cout << "--------------------------------------------------" << endl;
return 0;
}
- 注意:
- 封闭类对象生成时,先执行所有对象成员变量的构造函数,然后才执行封闭类的构造函数。
- 对象成员的构造函数调用次序和对象成员在类中的说明次序一致,与它们在成员初始化列表中出现的次序无关。
- 当封闭类的对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数。次序和构造函数的调用次序相反。
8. 常量成员函数
1) 如果不希望某个对象的值被改变(只读),则定义该对象的时候可以在前面加 const关键字。常量对象只能使用构造函数、析构函数和 有const 说明的函数,不能使用 其他非常量成员函数。
2)在类的成员函数说明后面可以加const关键字,则该成员函数成为常量成员函数。当const在函数名后面表示是常成员函数,该函数不能修改对象内的任何成员,也 不能调用非常量成员函数
3)常量成员函数的重载: 两个函数,名字和参数表都一样,但是一个是const,一个不是,算重载。
class Sample {
private:
int value;
public:
Sample() { }
//两个函数,名字和参数表都一样,但是一个是const,一个不是,算重载。
void SetValue() { }
void SetValue() const {
value = 0; // wrong,常成员函数不能修改对象内的任何成员
func(); //wrong,常成员函数也不能调用非常量成员函数
}
};
const Sample Obj1; // 常量对象
Sample Obj2; // 非常量对象
Obj2.SetValue();
Obj1.SetValue(); //常量成员函数的重载
注:
如果一个成员函数中没有调用非常量成员函数,也没有修改成员变量的值,那么, 最好将其写成常量成员函数,防止对象内的成员变量被修改。
9. 友元
- 友元的目的是使得不同类之间的成员函数可以获得相互访问对方私有成员变量的能力。
- 友元分为友元函数和友元类两种:
1) 友元函数: 一个类的友元函数可以访问该类的私有成员。
可以直接声明一个独立的友元函数,也可以将一个类的成员函数(包括析构函数,构造函数)定义为另一个类的友元。
#include <iostream>
using namespace std;
class CCar; //提前声明 CCar类,以便后面的CDriver类使用
class CDriver {
public:
void ModifyCar(CCar * pCar); //改装汽车
};
class CCar {
private:
int price;
int MostExpensiveCar(CCar cars[], int total); //直接声明一个独立的友元函数
friend void CDriver::ModifyCar(CCar * pCar); //将一个类的成员函数定义为另一个类的友元
};
void CDriver::ModifyCar(CCar * pCar) {
pCar->price += 1000; //汽车改装后价值增加
}
int CCar:: MostExpensiveCar(CCar cars[], int total){
//求最贵汽车的价格
int tmpMax = -1;
for (int i = 0; i < total; ++i)
if (cars[i].price > tmpMax)
tmpMax = cars[i].price;
return tmpMax;
}
int main() {
return 0;
}
2) 友元类: 如果A是B的友元类,那么A的成员函数都可以访问B的私有成员。
class CCar {
private:
int price;
friend class CDriver; //声明CDriver为友元类
};
class CDriver {
public:
CCar myCar;
void ModifyCar() {//改装汽车
myCar.price += 1000;//因CDriver是CCar的友元类,
//故此处可以访问其私有成员
}
};
int main() { return 0; }
注:友元类之间的关系不能传递,不能继承 !
第三章:运算符的重载
1. 运算符重载的基本概念
1)为什么需要运算符的重载?
- C++预定义的运算符,比如 + - * / % ^ & ~ ! | = << >> !=,只能用于基本数据类型(整型、实型、字符型、逻辑型…)的运算,不能用于对象之间的运算。
- 但有时为了代码的简洁易理解,我们也希望可以把这些运算符运用于对象之间的运算,例如:
complex_a和complex_b是两个复数对象;
求两个复数的和, 我希望能直接写:complex_a + complex_b
- 运算符重载的 目的 是:扩展C++中提供的运算符的适用范围,使之能作用于对象。
2)运算符重载的形式:
- 运算符重载的实质是 函数重载,可以重载为 普通函数,也可以重载为 成员函数,最终 把含运算符的表达式转换成对 运算符函数的调用。
- 运算符被多次重载时,根据 实参的类型 决定调用哪个运算符函数。
- 形式:
返回值类型 operator 运算符(形参表) {
……
}
3)
重载为成员函数时, 参数个数为运算符目数减一。
重载为普通函数时, 参数个数为运算符目数。
#include <iostream>
using namespace std;
class Complex {
public:
double real, imag;
Complex(double r = 0.0, double i = 0.0) :real(r), imag(i) {}
Complex operator-(const Complex & c);
};
Complex operator+(const Complex & a, const Complex & b) {
return Complex(a.real + b.real, a.imag + b.imag); //返回一个 临时对象
} // 重载为成员函数时, 参数个数为运算符目数减一
Complex Complex::operator-(const Complex & c){
return Complex(real - c.real, imag - c.imag); //返回一个临时对象
} // 重载为普通函数时, 参数个数为运算符目数
int main(){
Complex a(4, 4), b(1, 1), c;
c = a + b; //等价于 c = operator+(a,b);
cout << c.real << "," << c.imag << endl;
cout << (a.operator-(b)).real << "," << (a - b).imag << endl; //a-b 等价于 a.operator-(b)
return 0;
}
2. 赋值运算符 ‘=’ 的重载
- 为什么需要赋值运算符 ‘=’ 的重载?
有时我们希望即便赋值运算符两边的数据类型不匹配,但依然可以进行赋值运算,比如把一个int类型的变量赋值给一个Complex对象,或者把char *变量赋值给一个字符串对象,此时我们就是需要重载 “=”。
注:赋值运算符只能重载为成员函数!!!!
- ‘=’ 的重载时的注意事项:
a) 要在 class 类里,添加新的构造函数,避免初始化语句String s2 = "hello!";
报错。因为形如String s2 = "spring!"
是初始化语句(不是赋值语句,无法使用赋值运算的重载),因此需要编写一个参数符合要求的构造函数。
b) 浅拷贝和深拷贝问题:在 class String里添加成员函数,避免 S1=S2 导致的 S1.str 和 S2.str 指向同一地方带来的问题。
c) 返回值类型 的讨论:为什么是 String & ?
对运算符进行重载的时候,好的风格是应该尽量保留运算符原本的特性。1
考虑: a = b = c;
和 (a=b)=c; //会修改a的值
分别等价于:
a.operator=(b.operator=(c));
(a.operator=(b)).operator=(c); // a.operator=(b) 结果是 & a
d) 在用“=”赋值重载之外,当为类编写复制构造函数初始化对象的时候,也要考虑会产生和 S1=S2 一样的问题(因为如果利用 默认的复制构造函数 初始化后,两个str会指向同一个存储空间,导致delete错误的问题),我们用同样的方法解决。
#include <iostream>
#include <cstring>
using namespace std;
class String {
private:
char * str;
public:
String() :str(new char[1]) { str[0] = 0; } // 默认构造函数
String(String & s) { // d)
// 编写参数为 String 的复制构造函数的时候,会面临和"="同样的问题所以可以用同样的方法处理。
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
}
String(const char * a) {
// a) 编写参数为 char *或者const char *的构造函数,否则String s2 = "spring!";会报错!
str = new char[strlen(a) + 1];
strcpy(str, a);
}
String & operator = (const char * s) { // c) 返回值问题
//重载 “=” 以使得 obj = “hello” 能够成立
delete[] str;
str = new char[strlen(s) + 1];
strcpy(str, s);
return *this;
};
const char * c_str() { return str; };
String & operator = (const String & s) {
// b) 避免S1=S2执行之后,两者指向同一个空间所导致的空间的错误delete!
if (this == &s)
// 避免S=S时,delete [] str 所导致的空间的错误delete!
return *this;
delete[] str;
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
return *this;
}
~String() { delete[] str; }
};
int main() {
String s,s3;
s = "Good Luck,"; //等价于 s.operator=("Good Luck,");
cout << s.c_str() << endl;
String s1("hello!"); // d)
String s2 = "spring!"; // a)
s3 = s1; //b)
s = "Shenzhou 8!"; //等价于 s.operator=("Shenzhou 8!");
cout << s.c_str() << endl;
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
cout << s3.c_str() <<" 2号"<< endl;
return 0;
}
输出结果:
Good Luck,
Shenzhou 8!
hello!
spring!
hello! 2号
3. 运算符重载为友元函数
1)为什么需要把运算符重载为友元函数 ?
一般情况下,把运算符重载为类的成员函数,就能满足运算符重载的要求。但有的时候,重载为成员函数不能满足使用要求(因为重载为成员函数的时候,比如 c=c+5时,相当于 c = c.operator +(5); 但是当对 c=5+c时,c=5.operator +(5)明显会出错)。
所以,为了使得上述的表达式能成立,需要将 + 重载为普通函数。但是普通函数无法直接访问类对象的私有成员变量,因此需要将运算符+重载为友元。
#include <iostream>
#include <cstring>
using namespace std;
class Complex {
double real, imag;
public:
Complex(){ };
Complex(double r, double i) :real(r), imag(i) { };
friend Complex operator+(double r, const Complex & c);
};
Complex operator+(double r, const Complex & c) { //能解释 c+5
return Complex(r + c.real, c.imag);
}
int main() {
Complex c(5,4);
c = 5 + c;
}
4. 运算符重载实例:可变长整型数组
1)目的:
有时候我们希望数组的容量可以根据需求变化,可变可大可小。但是虽然运用动态内存分配可以解决问题,但却要时时用delete来析构动态分配的内存,很麻烦。
所以,我们需要开发出一种动态可变长度的数组类。
此处,我们利用一个具体的例子来说明这个类的具体应用和定义方式。
2) 举例:
为了能够成功定义动态可变长度的数组类,我们需要完成以下任务:
a). 定义一个指针成员变量,用来动态分配的内存存放数组元素。
b). 赋值运算符"="重载,用于数组对象间的赋值,实现深拷贝的操作(拷贝前后的指针指向同一片存储空间)。
c). 重载“[ ]”
- 因为C++预定义的中括号[ ]只能用于普通数据数组的名字之后,而不能用于类名之后,所以 我们需要对其进行重载。
- 应当注意:[ ]中括号是双目运算符,一个操作数在中括号内, 一个操作数在中括号外面,代表数组名称。此时我们将[ ]在成员函数中重载,所以只需要一个操作数,即数组的下标。
- 返回值是什么类型? 需要满足的要求是,既可以对类数组赋值,又可以取值,如
n = a[i] 和a[i] = 4
; 所以,返回值应该是引用类型!!!
d). 复制构造函数不能直接用缺省的复制构造函数,所以要自行编写(原因是复制构造函数需要完成深拷贝的任务,防止拷贝前后的指针指向同一片存储空间带来的delete出错)。
#include <iostream>
using namespace std;
class CArray {
int size; //数组元素的个数
int *ptr; //指向动态分配的数组 a)
public:
CArray(int s = 0); //s代表数组元素的个数
CArray(CArray & a); //d)
~CArray();
void push_back(int v); //用于在数组尾部添加一个元素v
CArray & operator=(const CArray & a); // b)
//用于数组对象间的赋值
int length() { return size; } //返回数组元素个数
int & operator[](int i) { // c)
//返回值为 int 不行!不支持 a[i] = 4
//用以支持根据下标访问数组元素,
// 如n = a[i] 和a[i] = 4; 这样的语句
return ptr[i];
}
};
CArray::CArray(int s) :size(s) {
if (s == 0)
ptr = NULL;
else
ptr = new int[size]; //构造函数定义一个有具体长度的的数组空间
}
CArray::CArray(CArray & a) {
if (!a.ptr) {
ptr = NULL;
size = 0;
return;
}
ptr = new int[a.size];
memcpy(ptr, a.ptr, sizeof(int) * a.size);
size = a.size;
}
CArray::~CArray() {
if (ptr) delete[] ptr;
}
CArray & CArray::operator=(const CArray & a) { //赋值号的作用是使“=”左边对象里存放的数组,大小和内容都和右边的对象一样
if (ptr == a.ptr) //防止a=a这样的赋值导致出错
return *this;
if (a.ptr == NULL) { //如果a里面的数组是空的
if (ptr) delete[] ptr;
ptr = NULL;
size = 0;
return *this;
}
if (size < a.size) { //如果原有空间够大,就不用分配新的空间
if (ptr)
delete[] ptr;
ptr = new int[a.size];
}
memcpy(ptr, a.ptr, sizeof(int)*a.size);
size = a.size;
return *this;
}
void CArray::push_back(int v) { //在数组尾部添加一个元素
if (ptr) {
int * tmpPtr = new int[size + 1]; //重新分配空间
memcpy(tmpPtr, ptr, sizeof(int)*size); //拷贝原数组内容
delete[] ptr;
ptr = tmpPtr;
}
else //数组本来是空的
ptr = new int[1];
ptr[size++] = v; //加入新的数组元素
}
int main() { //要编写可变长整型数组类,使之能如下使用:
CArray a; //开始里的数组是空的
for (int i = 0; i < 5; ++i)
a.push_back(i);
CArray a2, a3;
a2 = a;
for (int i = 0; i < a.length(); ++i)
cout << a2[i] << " ";
a2 = a3; //a2是空的
for (int i = 0; i < a2.length(); ++i) //a2.length()返回0
cout << a2[i] << " ";
cout << endl;
a[3] = 100;
CArray a4(a);
for (int i = 0; i < a4.length(); ++i)
cout << a4[i] << " ";
return 0;
}
5. 流插入运算符和流提取运算符的重载
1)cin>> 和 cout<< 的本质是什么?
-
cin 和 cout 分别是在 头文件 iostream 中定义的, istream类和ostream类的对象。
-
“ <<” 能用在cout 上是因为,在iostream里对 “ <<” 进行了重载。同理,“ >>” 能用在cin 上是因为,在istream里对 “ >>” 进行了重载。
-
重载为成员函数的格式大致为:
void ostream::operator<<(int n) {
…… //输出n的代码
return;
}
所以,cout << 5 ;
即为 cout.operator<<(5);
2) 怎么才能使得 连续流插入/提取运算符 的重载成立?比如 cout << 5 << “this”;
- a) 首先应该考虑流运算重载的成员函数的返回值:
返回值应该为 ostream & 或 istream & ,这样返回值就能保持为cout 或 cin,使得流插入/提取运算得以连续重载。
ostream & ostream::operator<<(int n) {
…… //输出n的代码
return * this;
}
istream & ostream::operator<<(const char * s ) {
…… //输入s的代码
return * this;
}
- b) 本质上的函数调用的形式是:
cout.operator<<(...).operator<<(...);
- c) “<<” 和 “>>”的只能重载为全局函数,不能够再把他们重载成ostream类 或 istream类 的成员函数,因为这个两个类都已经写好了。所以,只能把这两个流运算的重载写为全局函数。
- d) 同时,为了使得重载的全局函数能够访问类对象里的private成员变量,我们需要把重载的全局函数声明为类对象的友元 (friend)。
举例如下:
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
class Complex {
double real, imag;
public:
Complex(double r = 0, double i = 0) :real(r), imag(i) { };
friend ostream & operator<<(ostream & os, const Complex & c);
friend istream & operator>>(istream & is, Complex & c); //d)
};
ostream & operator<<(ostream & os, const Complex & c){ // a)
os << c.real << "+" << c.imag << "i"; //以"a+bi"的形式输出
return os;
}
istream & operator>>(istream & is, Complex & c) { //c)
string s;
is >> s; //将"a+bi"作为字符串读入, “a+bi” 中间不能有空格
int pos = s.find("+", 0);
string sTmp = s.substr(0, pos); //分离出代表实部的字符串
c.real = atof(sTmp.c_str()); //atof库函数能将const char*指针指向的内容转换成 float, c_str():生成一个const char*指针,指向以空字符终止的数组
sTmp = s.substr(pos + 1, s.length() - pos - 2); //分离出代表虚部的字符串
c.imag = atof(sTmp.c_str());
return is;
}
int main() {
Complex c;
int n;
cin >> c >> n;
cout << c << "," << n;
return 0;
}
6. 类型转换运算符的重载
- 目的:是对 类对象 进行转换成数据类型。
- 在任何可以出现重载后变量转换的变量类型的地方,如果出现的是一个类的对象,那么这个对象就会被自动识别,调用类型转换运算符的重载,将对象转换为重载后的数据类型。(类数据的自动识别转换!)
这就解释了,为什么double n = 2 + c;
会编译通过。
#include <iostream>
using namespace std;
class Complex{
double real, imag;
public:
Complex(double r = 0, double i = 0) :real(r), imag(i) { };
double operator double() { return real; }
//重载强制类型转换运算符 double
};
int main()
{
Complex c(1.2, 3.4);
cout << (double)c << endl; //输出 1.2
double n = 2 + c; //等价于 double n=2+c.operator double()
cout << n; //输出 3.2
}
7. 自增、自减运算符的重载
1) 自增运算符++、自减运算符 – 有前置/后置之分,为了重载区分,C++规定:
- 前置运算符 作为 一元 运算符重载
- 重载为成员函数:
T & operator++();
T & operator–(); - 重载为全局函数:
T1 & operator++(T2);
T1 & operator- -(T2);
- 重载为成员函数:
- 后置运算符作为 二元 运算符重载,多一个无用参数:
- 重载为成员函数:
T operator++(int);
T operator–(int); - 重载为全局函数:
T1 operator++(T2,int );
T1 operator- -( T2,int);
- 重载为成员函数:
- 但是在没有后置运算符重载而有前置重载的情况下,在vs中, obj++ 也调用前置重载。
- 编写注意事项:
a)前置运算符返回值是被操作变量的 引用,即++a,–a的返回值是 &a。比如 (++a)=1;执行之后,a=1,因为 ++a返回的是 a 的引用。相反,后置运算符的返回值是一个 临时变量。
b) 类型强制转换运算符 被重载时 不能写返回值类型,实际上其返回值类型就是该类型强制转换运算符代表的类型。
c) 通过函数编写发现,前置运算重载 的运行效率要高于后置运算。因为前置运算并没有生成新的局部对象 CDemo tmp(引发构造函数),返回的时候直接返回引用。相比而言,后置运算会生成新的局部对象,引发构造函数的调用,返回的时候也会返回新生成的对象,同时再次调用构造函数,两次调用构造函数的开销导致了运行效率的降低。
注:
在使用自增自减运算的时候,当变量不是对象的时候,前置和后置的运行效率基本相同。
但当变量是对象的时,前置运算符的执行效率要高于后置运算符。
所以在能够满足编程正确性的情况下,写成前置运算要比后置运算更优雅快捷!
3)举例:
#include <iostream>
using namespace std;
class CDemo {
private: int n;
public:
CDemo(int i = 0) :n(i) { }
//a)
CDemo & operator++(); //用于前置形式 (成员函数)
CDemo operator++(int); //用于后置形式 (成员函数)
operator int() { return n; } // 类型强制转换运算符
friend CDemo & operator--(CDemo &); //用于前置形式(全局函数)
friend CDemo operator--(CDemo &, int); //用于后置形式(全局函数)
};
CDemo & CDemo::operator++() { //前置 ++
n++;
return *this;
} // ++s即为: s.operator++();
CDemo CDemo::operator++(int k) { //后置 ++
CDemo tmp(*this); //记录修改前的对象
n++;
return tmp; //返回修改前的对象
} // s++即为: s.operator++(0);
CDemo & operator--(CDemo & d) {//前置--
d.n--;
return d;
} //--s即为: operator--(s);
CDemo operator--(CDemo & d, int) {//后置--
CDemo tmp(d); // c)
d.n--;
return tmp;
} //s--即为: operator--(s, 0);
int main() {
CDemo d(5);
cout << (d++) << ","; //等价于 d.operator++(0);
cout << d << ",";
cout << (++d) << ","; //等价于 d.operator++();
cout << d << endl;
cout << (d--) << ","; //等价于 operator--(d,0);
cout << d << ",";
cout << (--d) << ","; //等价于 operator--(d);
cout << d << endl;
return 0;
}
8. 其他注意事项
- C++不允许定义新的运算符 ;
- 重载后运算符的含义应该符合日常习惯;
complex_a + complex_b
word_a > word_b
date_b = date_a + n - 运算符重载不改变运算符的优先级;
- 以下运算符不能被重载:“ .” 、“ .*” 、“ ::” 、“ ?:” 、 sizeof;
- 重载运算符 ()、 []、 -> 或者赋值运算符 = 时,运算符重载函数必须声明为类的 成员函数。
注脚的解释在C++里赋值运算符的返回值是"="左边变量的引用! 所以我们重载后的赋值运算符依然要保持这个性质!(即重载后的 返回值一定要是引用!!!) ↩︎