程序设计与算法(三)C++面向对象程序设计 北京大学 郭炜

《多喝水》




01~引用:int & r1=a;
“别名”
从一而终, 接下来的“=”都是赋值含义
一定要初始化
只能引用变量
常引用:const int & a;
不能通过常引用修改其引用的内容,但可通过其他方式修改被引用的内容
exgcd(int a, int b, &x, &y)里的就是别名
02~const:
相比define有类型检查,define只是替换
常量指针 const int*p = &n
常量指针可赋值给非常量指针,反过来不行(但可强制转换)
防止自己在函数里改了不应修改的内容

03~动态内存分配:new<->delete
- int * P1 = new int; // 动态分配一个变量
- delete p1;
- int * P2 = new int[N]; // 一个数组
- // new返回值类型都是T*
- delete []p2; //中括号不能忘记,不然分配来的数组没有完全释放~
- int * P3; //不导致int类型的对象生成
- CRectangle * P4; //不导致对象生成, 不会调用构造函数
- CRectangle * P4 = new CRectangle(2,4); //调用构造函数
04~内联函数
inline int Max(int a, int b) { 函数体语句很少 }
编译时不生成函数调用语句,而是把原函数直接贴在调用位置
可执行程序的体积略微增大
04~函数重载
几个函数名称相同,使得函数命名变得简单
只要参数表不一样,就不算重复定义
04~缺省参数:
void f(int x1, int x2 = 2, int x3 = 3) { }
f(10) //f(10, 2, 3)
f(10, , 8) //error
提高可扩充性 void circle(int r, int x, int y, sring color = "black")
注意避免有函数重载时的二义性(06~)
05~结构化程序设计
不足:没有封装和隐藏的概念
具体表现在,函数和其所操作的数据结构没有直观的联系
05~类和对象:带函数的结构体
封装、隐藏、抽取重用
解决~函数跟其操作的数据结构没有直观联系
- class CRectangle //自定义类型名 //class换成struct也没啥~
- {
- public :
- int w, h; //成员变量
- // 成员函数(不属于对象里,仅有一份)
- int Area(); //算面积 ,函数内容在class之外
- int Perimeter() { return 2*(w+h); } //算周长
- void Init(int _w, int _h) { w = _w; h = _h; } //初始化
- }; //分号~
- int CRectangle:: int Area() { return w*h; } //写在类之外的成员函数前要加上”类名::“,不然怎么知道对应哪个类的成员函数~
- int main(void)
- {
- int w, h;
- CRectangle r; //r是对象(类的实例),所占用的内存空间>=所有成员变量之和
- cin >> w >> h;
- r.Init(w, h);
- cout << r.Area() << endl << r.Perimeter();
- return 0;
- }
对象间的运算:
可赋值,不能比较,除非重载运算符
成员变量和成员函数的使用:
对象名.成员名 //上面已经用到~
指针->成员名
- CRectangle r1, r2;
- CRectangle * p1 = & r1, * p2 = & r2;
- p1->w = 5;
- p2->Init(5, 4);
引用名.成员名
- CRectangle & rr = r2;
- rr.Init(5, 4);
06~ private和public
成员变量和成员函数可分开写
int CRectangle:: int Area() { return w*h; }
类成员的可访问范围
- private (默认)
- public
- protected (以后再说)
私有成员变量在主函数里不能通过”类名::成员名“访问,但可通过公有成员函数修改或打印其值(视频12中有例子)
07~ 构造函数:
X::X(所有成员变量对应的参数) {每个成员变量都有了自己的值}
名字与类名相同
对对象进行初始化,自动
- class Complex {
- private :
- int real, imag;
- public :
- Complex(int r, int i = 0);
- };
- Complex::Complex(int r, int i) {
- real = r; imag = i;
- }
- Complex c1; //error
- Complex * pc = new Complex; //error
- Complex c1(2); //OK
- Complex c1(2, 4); //OK
- Complex * pc = new Complex(3, 4); //error
本文03~也中有相关内容~
08~ 复制构造函数——初始化
X::X( X & 对象)
X::X( const X & 对象)
只有一个参数,且必须是引用——即同类对象的引用
如果你没写复制构造函数,编译器会生成默认的复制构造函数(浅拷贝<或叫位拷贝>但我们一般需要深拷贝<或叫值拷贝>)
【总结】无参构造函数不一定存在,但复制构造函数一定存在
起作用的三种情况
Complex c2(c1); //当一个对象去初始化另一个对象时
上式<=> Complex c2 = c1; //这不是赋值语句,而是初始化语句
【注意】对象间赋值不导致调用复制构造函数
void f(A a); f(a2); //对象做参数,这个形参a被a2初始化——复制构造函数
【注意】形参不一定是实参的拷贝,比如当你自己写的复制构造函数并没有赋值语句
A f(void); //对象作返回值,编译器创建临时对象,临时对象被初始化
【注意】有的编译器会进行优化,省略返回值的临时变量
这样太慢了,所以使用常量(const)引用(&)参数
09(1)~ 类型转换构造函数
Complex(一个参数) {所有成员变量都有了自己的值}
类型转换构造函数特点:
1.是构造函数
2.只有一个参数
3.但不是复制构造函数
比如Complex初始化需要两个参数i和r,当主函数中写道“c1 = 9”时,通过这种函数把9转化为和Complex对象格式相同的样子,视频中就是(9,0)
“类型转换构造函数”只是一类用法的名字。当需要的时候,编译器自动调用。
09(2)~ 析构函数
~Complex() {}
没有参数
每个类仅有一个
而构造函数每个类可有多个(重载)
如果你没写析构函数,编译器会生成析构函数(不做任何操作)
10~ 构造/析构函数调用时机
(复制)构造函数:创建新对象(初始化)
类型转换构造函数:如“Complex a = 5; ”编译器将5转换成临时对象,临时对象把值给a,临时对象再被析构
析构函数:对象生命周期结束(区分:全局变量、静态局部变量、局部变量的生命周期)
【注意】使用new分配来的对象只要不delete,就不会消亡,也就是不被析构
函数参数传递对象时(形参列表或函数返回),如果不是引用,则有两个临时对象被创建,形参列表的那一个在函数调用结束时被析构,被返回的那个临时对象在调用语句结束后也被析构
备注:有的编译器为了优化,不生成返回值临时对象,就少调用一对复制构造函数和析构函数~
11~ this指针
指向成员函数所作用的对象
静态成员函数中不能使用
19~实例:实现一个CArray类(可变长数组)(就是vector)
- # include <iostream>
- # include <cstdio>
- using namespace std;
- class CArray
- {
- int size;
- int cnt;
- int * ptr;
- public:
- CArray() //构造函数,可以用缺省参数方法,这样创建对象时如果有规定大小,写在括号里就行啦~
- {
- size = 2;
- cnt = 0;
- ptr = new int[2];
- printf("构造\n");
- }
- CArray(const CArray & a) //复制构造函数
- {
- size = a.size;
- cnt = a.cnt;
- //自己写复制构造函数,而不用编译器缺省出来的原因是我需要深拷贝
- ptr = new int[size];
- for (int i = 0; i < cnt; i++)
- {
- ptr[i] = a.ptr[i];
- }
- printf("复制构造\n");
- }
- void operator=(const CArray & a) //返回值最好是&,方便连等
- {
- //还要避免 a = a
- //如果构造函数默认size是0的话,空数组的赋值单独写
- //原有空间不够用时再分配新空间,效率高
- size = a.size;
- cnt = a.cnt;
- ptr = new int[size];
- for (int i = 0; i < cnt; i++)
- {
- ptr[i] = a.ptr[i];
- }
- printf("复制构造:运算符“=”重载\n");
- }
- ~CArray() //析构函数
- {
- delete[]ptr;
- printf("析构\n");
- }
- void push_back(int x)
- {
- if (cnt == size) //放不下了
- {
- int * oldptr = ptr;
- ptr = new int[size * 2];
- for (int i = 0; i < size; i++)
- {
- ptr[i] = oldptr[i];
- }
- delete[] oldptr;
- ptr[size] = x;
- cnt++;
- size *= 2;
- }
- else
- {
- ptr[cnt] = x;
- cnt++;
- }
- return;
- }
- int length()
- {
- return cnt;
- }
- int & operator [](int i) //返回类型写对啦~ (非引用的返回值不能做左值)
- {
- return ptr[i];
- }
- };
- int main(void)
- {
- CArray a, a2, a3;
- a.push_back(1); a.push_back(2); a.push_back(3);
- a2 = a; // “=”重载的复制构造,为了防止浅拷贝
- printf("遍历a2:%d %d %d\n", a2[0], a2[1], a2[2]); //重载“[]”
- a[1] = 5;
- printf("遍历a:%d %d %d\n", a[0], a[1], a[2]);
- CArray a4(a);//复制构造函数
- printf("a4现在有%d个元素\n", a4.length());
- printf("遍历a4:%d %d %d\n", a4[0], a4[1], a4[2]);
- return 0;
- }
- Output:
- 构造
- 构造
- 构造
- 复制构造:运算符“=”重载
- 遍历a2:1 2 3
- 遍历a:1 5 3
- 复制构造
- a4现在有3个元素
- 遍历a4:1 5 3
- 析构
- 析构
- 析构
- 析构
20~ "<<"和">>"的重载
cout是在iostream中定义的ostream类的对象
例题:
- # include <iostream>
- using namespace std;
- class Complex
- {
- public:
- int real, imag;
- };
- ostream & operator << (ostream & o, const Complex & c)
- {
- o << c.real << "+" << c.imag << "i";
- return o;
- }
- istream & operator >> (istream & i, Complex & c)
- {
- scanf_s("%d+%di", &c.real, &c.imag);
- return i;
- }
- int main(void)
- {
- Complex c1, c2;
- cin >> c1 >> c2;
- cout << c1 << endl << c2 << endl;
- return 0;
- }
弹幕中的问题:既然ostream类已经不能再加入新的成员函数,为什么不能把运算符重载函数写为Complex类的成员函数,而一定要将其写成全局函数?
把"<<"重载为Complex类的成员函数只需要1个参数("<<"的目数减1),那就得写成下面这个样子:
ostream & operator << (ostream & o)
{
o << real << "+" << imag << "i";
return o;
}
但是:
对双目运算符而言,成员运算符重载函数的形参表中仅有一个参数,它作为运算符的右操作数。另一个操作数(左操作数)是隐含的,是该类的当前对象,它是通过 this 指针隐含地递给函数的。(视频16的08:30——”a运算符b“等价于”a.operator运算符b“)
所以只有1个参数的形参表没法写,所以一定要将"<<"重载函数写成全局函数~
21~ 类型转换运算符的重载
opreator double() {return real;} //不写返回类型
cout << (double)c; //可显式转换
double n = 2 + c; //可隐式转换
22~ "++" "--" 的重载
为了区分前置和后置,前置运算符视为一元,后置运算符视为二元(第二个形参无意义随意写,但要有)
21、22综合例子:
- # include <iostream>
- using namespace std;
- class CDemo
- {
- private:
- int n;
- public:
- CDemo(int _n) { n = _n; }
- operator int() { return n; }
- friend CDemo & operator++ (CDemo & d);
- friend CDemo & operator--(CDemo & d);
- friend CDemo operator++ (CDemo & d, int);
- friend CDemo operator-- (CDemo & d, int);
- ~CDemo() {}
- };
- CDemo & operator++ (CDemo & d) { d.n++; return d; }//前置加加
- CDemo & operator-- (CDemo & d) { d.n--; return d; }
- CDemo operator++ (CDemo & d, int) { CDemo tmp(d); d.n++; return tmp; }//后置加加 //CDemo tmp(d); 这一句,使用了编译器缺省的复制构造函数
- CDemo operator-- (CDemo & d, int) { CDemo tmp(d.n); d.n--; return tmp; }
- int main(void)
- {
- CDemo d(5);
- cout << (d++) << endl;
- cout << d << endl;
- cout << (++d) << endl;
- cout << d << endl; //5677
- cout << (d--) << endl;
- cout << d << endl;
- cout << (--d) << endl;
- cout << d << endl; //5677
- return 0;
- }
【注意】++i和i++在返回值和效率上的差别:
++i:返回是i的引用,且效率高
i++:返回值是临时变量,且效率低
【运算符重载的总结】c++不能定义新的运算符、重载要符合日常习惯、重载不改变运算符优先级、有些运算符不能被重载(点号、点星、两冒号、问号冒号、sizeof)、有些运算符必须声明为类的成员函数(小括号、中括号、箭号、赋值等号)