C++深入理解 面向对象部分
一、补充知识
现在开始刷 北京大学 程序实际与算法三 视频面向对象部分学习,记录自己不懂的知识,这里以后打算学习 C++ 了,把这门语言吃透再学习新的,这里使用的运行环境为Code::blocks。这几天找到一个 C++ 项目《一个基于C++11简单易用的轻量级网络编程框架》,链接如下:https://gitee.com/xia-chu/ZLToolKit。
因为8月29日下午有一个 360 测试工程师 的笔试,想努努力。做了一下题目,里面算法部分居多,而且主要用 C++ 和 C 语言,对指针和一些部分理解还算深刻。而且为了做上面的 C++ 项目,先把基础打好,磨刀不误砍柴功,自己一定要稳住。开始学习,肝起来。
1.1 基本语法和语句
1.1.1 引用的概念
定义一个引用,并初始化为某个引用变量。注意:1. 引用开始时将其初始化为某个变量;2. 初始化以后,就一直引用,不再引用其他变量; 3. 引用只能引用变量不能引用常量和表达式。定义为,
类型名 & 引用名 = 某变量名;
例如,
int n = 4, b = 7;
int &r = n; // r 引用了 n , r 的类型为 int&
---> 对某个变量的引用,等价于这个变量,相当于该变量的一个别名。
int n = 7;
int &r = n;
int &r1 = r; // r1 引用 r, r 引用 n
r1 = b; // 引用从一而终,不变化
r = 4; cout << r << n << endl; // 4 4
n = 5; cout << r << n << endl; // 5 5
在 C++ 语言中编写一个函数交换两个整型数据,如下
#include <iostream>
using namespace std;
void swap(int &a, int &b){
int temp;
temp = a; a = b; b = temp;
}
int n = 4;
int &setValue(){return n;} // 引用作为函数返回值
int main(){
int n1, n2;
swap(n1, n2); // n1 和 n2 值被交换
// 引用作为函数返回值
setValue() = 40;
cout << n << endl; // 那么 n = 40.
return 0;
}
引用还可以作为函数值的返回值,如上面所示。定义引用时,前面加上 const 关键字,即为 “常引用”。不能通过常引用去修改引用内容 ,如下,
const T & 和 T & 是不同的类型
T & 类型和或者 T 类型可以用来初始化 const T & 类型。
const T 类型的常变量和 const T & 类型的引用不能用来初始化 T & 类型的引用,除非进行强制类型转换。
1.1.2 const 关键字
注意1: 定义常量指针,1. 不可通过常量指针修改其指向内容;2. 不能把常量指针赋值给非常量指针,如下
/* 1. */
int n, m;
const int *p = & n;
*p = 5; // F 编译出错
n = 4; // R 是可以的
p = & m; // R 常量指针的指向是可以变化的
/* 2. */
const int *p1; int p2;
p1 = p2; // R 可以的
p2 = p1; // F 不可以,那么修改 p2 指针内容,p1也被修改了
p2 = (int*)p1; // R 可以,强制类型转换
注意2: 函数参数为常量指针时,可避免函数内部不小心改变参数指针所指地方的内容,如下
void myPrintf(const char *p){
strcpy(p, "this"); // 编译出错
printf("%s", p);
}
注意3: 不能通过常引用修改其引用的变量,如下
int n;
const int & r = n;
r = 5; // 错误
n = 4; // 正确
1.1.3 动态内存分配
用 new 运算符实现动态内存分配,用 new 运算符返回值类型为类型的指针 T* 。举例如下,
/* 第一种用法,分配一个变量 */
// T 是任意类型名, P 是类型为 T* 的指针。
// 动态分配出一片大小为 sizeof(T) 字节的内存空间,并将内存空间起始地址赋给 P,
P = new T;
int *pn;
pn = new int;
*p = 5;
/* 第二种用法,分配一个数组 */
// T 是任意类型名, P 是类型为 T* 的指针, N 为数组个数。
// 动态分配出一片大小为 N*sizeof(T) 字节的内存空间,并将内存空间起始地址赋给 P,
P = new T[N];
int *pn;
pn = new int[100];
pn[0] = 30;
用 delete 运算符实现空间的释放,用 delete 指针必须指向 new 出来的空间。举例如下,
int *p = new int;
*p = 5;
delete p; // 正确
delete p; // 一片空间不能被 delete 多次
int *p = new int[20];
p[0] = 1;
delete[] p; // 要加中括号delete数组
1.2 内联函数,函数重载,函数缺省参数
1.2.1 内联函数
函数调用是有时间开销的,如果函数语句很少,执行很快,若函数被反复执行,相比之下调用函数这个开销就比较大。为了减少函数调用开销,引入内联函数机制。编译器会将函数代码直接插入语句处,不会产生调用函数语句,如
// 在函数定义前面加上 “inline” 关键字,定义了内联函数。
inline int max(int a, int b){
if(a > b) return a;
else return b;
}
1.2.2 函数重载
一个或多个函数,名字相同,然而参数个数或参数类型不相同,这叫函数重载,
// 以下三个函数是重载关系:
int max(double f1, double f2);
int max(int f1, int f2);
int max(int f1, int f2, int f3);
函数重载使得函数命名简单,编译器根据调用函数实参类型确定调用哪个函数。
1.2.3 函数的缺省参数
定义函数时,可以让最右边连续的若干参数有缺省值,那么调用函数时,若相应位置不写参数,参数就是缺省值。
// 以下三个函数是重载关系:
void func(int x1, int x2 = 2, int x3 = 4);
func(10); // func(10, 2, 4);
func(10, 8) // func(10, 8, 4);
设置函数可缺省目的在于提高于程序的可扩充性。
二、面向对象部分
2.1 类和对象的基本概念和用法
C 语言使用结构程序化设计: 程序 = 数据结构 + 算法。程序是由全局变量以及众多相互调用的函数组成; 算法以函数的形式实现,用于对数据结构进行操作。
结构化程序设计中,1. 函数和其操作的数据结构没有直观的联系; 随着程序规模增加,程序逐渐难以理解,很难一下子看书数据结构有哪些函数对它操作,函数操作哪些数据结构,任何两个函数之间的调用关系。2.没有封装和隐藏的概念.当变量定义有改动时,要把所有访问该变量的语句找出来修改,十分不利于程序维护,扩充。3.*难以查错, 某个数据结构不正确时,难以找出哪个函数导致的。4. 有相同的地方要抽取出来,增加代码的数量。
总之,结构化程序,在规模庞大,变得难以理解,难以扩充,难以查错,难以重用。
面向对象程序设计方法,将某类客观事物共同特点归纳出来,形成数据结构; 将这类事物所能进行的行为也归纳出来,行为成为一个个函数,用来操作数据结构。把他们组合在一起行为一个类。使得数据结构和函数形成紧密的关系,叫做封装。面向对象设计具有 抽象, 封装, 继承, 多态 四个基本特点。
2.2 类的介绍
举个例子,对矩形进行抽象描述,
#include <iostream>
using namespace std;
class Rectangle{
public:
int wid, high;
int area(){
return wid*high;
}
int perimeter(){
return 2*(wid + high);
}
void Init(int w_, int h_){
wid = w_; high = h_;
}
}; // 必须有分号
int main(){
int wid, high;
Rectangle rec;
cin >> wid >> high;
rec.Init(wid, high);
cout << rec.area() << endl;
cout << rec.perimeter() << endl;
return 0;
}
通过类,可以定义变量。类定义出来的变量,也成为类的实例,就是所说的 “对象” 。和结构体变量一样,对象所占所占用的内存空间的大小,等于所有成员变量的大小之和。sizeof(Rectangle) = 8 , 函数不占据空间. 每个对象各有自己的存储空间,一个对象变量改变了,不会影响另外一个对象。使用类的成员变量和函数,
用法一 对象名.成员名;
用法二 指针->成员名;
用法三 引用名.成员名;
// 用法二,举个例子
Rectangle r1, r2;
Rectangle *p1 = &r1;
Rectangle *p2 = &r2;
p1 -> w = 5;
p2 -> Init(5, 4);
// 用法三,举个例子
Rectangle r1;
Rectangle & rr = r2;
rr.w = 5;
rr.Init(5, 4); // rr值变了,r2也变了
类的成员函数和类的定义可以分开写,
class Rectangle{
public:
int wid, high;
int area(); // 成员函数仅在这声明
int perimeter();
void Init(int w_, int h_);
}; // 必须有分号
// 类名::函数,这样通过对象来调用
int Rectangle::area(){
return wid*high;
}
int Rectangle::perimeter(){
return 2*(wid + high);
}
void Rectangle::Init(int w_, int h_){
wid = w_; high = h_;
}
2.3 类成员的可访问范围
在类的定义中,用几个关键字来说明类成员可访问范围,
-private: 私有成员,只能成员函数内访问
-public: 公有成员,可以在任何地方访问
-protected: 保护成员,以后再说
class className{
// 缺省成员和方法是私有的
private:
// 私有属性和方法
public:
// 公有属性和方法
protected:
// 保护属性和方法
}
在类的成员函数内部,能够访问当前对象的全部属性,同类其它对象的全部属性,函数。
在类的成员函数以外的地方,只能够访问该类对象的共有成员。举个例子
#include <iostream>
#include <cstring>
using namespace std;
class Employee{
private:
char emName[30];
public:
int salary;
void setName(char *name);
void getName(char *name);
int aveSalary(Employee e1, Employee e2);
};
void Employee::setName(char *name){
strcpy(emName, name); // 可以的
}
void Employee::getName(char *name){
strcpy(name, emName); // 可以的
}
int Employee::aveSalary(Employee e1, Employee e2){
cout << e1.emName << endl;
salary = (e1.salary + e2.salary)/2; // 可以的
}
int main(){
Employee em, em1;
// strcpy(em.emName, "Tom"); // 编译错误,不能访问私有成员
em.setName("Tom");
em1.setName("Jack");
em.salary = 5000;
em1.salary = 7000;
int sal = em.aveSalary(em, em1);
cout << sal << endl;
return 0;
}
结果如下
成员函数也可以重载和形参缺省。
2.4 构造函数
如果定义了构造函数,则编译器不生成默认的无参数的构造函数。对象生成时构造函数自动被调用。对象一旦生成,就再也不能在上面执行,一个类可以有多个构造函数。例如
#include <iostream>
#include <cstring>
using namespace std;
class Complex{
private:
double real, imag;
public:
Complex(double r, double i = 0);
}
Complex::Complex(double r, double i){
real = r; imag = i;
}
int main(){
Complex c1; // 错误,缺少构造函数参数
Complex *pc = new Complex; // 没有参数
Complex c1(2); // 可以,有缺省形参
}
构造函数在数组中使用,如下
#include <iostream>
#include <cstring>
using namespace std;
class Sample{
int x;
public:
Sample(){
cout << "Constructor 1 called" << endl;
}
Sample(int n){
x = n;
cout << "Constructor 2 called" << endl;
}
};
int main(){
Sample array1[2];
cout << "step1" << endl;
Sample array2[2] = {4,5};
cout << "step2" << endl;
Sample array3[3] = {3};
cout << "step3" << endl;
Sample *array4 = new Sample[2];
delete[] array4;
return 0;
}
/* 第二部分 */
class Test{
public:
Test(int n){}
Test(int n, int m){}
Test(){}
};
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) 初始化, 有一个空指针检测不到指哪里
结果如下,
2.5 复制构造函数
基本概念:只有一个参数,即对同类对象的引用。形如
x::x (x &) 或者 x::x(const x &)
二者选一,后者能以常量对象作为参数
如果定义没有复制构造函数,那么编译器生成默认复制构造函数。默认的复制构造函数完成复制功能。举例
/* 1. */
class Complex{
private:
double real, imag;
};
Complex c1; // 调用缺省无参数构造函数
Complex c2(c1); // 调用缺省的复制构造函数,将 c2 初始化成和 c1 一样
/* 2. */
// 如果调用定义自己的复制构造函数,则默认的复制构造函数不存在
class Complex{
private:
double real, imag;
Complex(const Complex & c){
real = c.real;
imag = c.imag;
cout << "Copy constructor called";
}
};
Complex c1; // 调用缺省无参数构造函数
Complex c2(c1); // 调用自己定义的复制构造函数,输出 Copy constructor called
}
/* 3. */
// 不允许有形如 x::x(x) 的构造函数,例如
class Sample{
Sample(Sample C){
} // 错误的,不允许这样的构造函数
}
复制构造函数起作用的三种情况,
/* 1. */
// 1 当用一个对象去初始化同类的另一个对象时
Complex c2(c1); // 和下面等价
Complex c2 = c1; // 初始化语句,不是赋值语句,和上面等价
/* 2. */
// 2 如果某函数有一个参数是类 A 的对象,
// 那么该函数被调用时,类 A 的复制构造函数将被调用
class Sample{
public:
Sample(){};
// 复制构造函数并没有做复制的作用
Sample(Sample & s){
cout << "Copy constructor called" << endl;
}
}
void Func(Sample s1){} // 调用复制构造函数
int main(){
Sample s2;
// s1 和 s2 一样,形参不是实参的拷贝
Func(s2); // 程序输出为 Copy constructor called
return 0;
}
/* 3. */
// 3 如果函数的返回值是类 A 的对象时,则函数返回时,
// A 的复制构造函数被调用
class Sample{
public:
int v;
Sample(int n){v = n};
Sample(const Sample & a){
v = a.v;
cout << "Copy constructor called" << endl;
}
}
Sample Func(){
Sample b(4);
return b;
}
int main(){
// 返回值对象看作是 b 的一个复制品,并且对象返回时调用复制构造函数
cout << Func().v << endl; return 0;
}
输出结果:
Copy constructor called
4
常量引用参数的引用,
void fun(myClass obj_){
cout << "func" << endl;
}
// 1 这样的函数,调用时形参会引发复制构造函数的调用,开销比较大
// 2 可以考虑 myClass & 引用类型作为参数
// 3 如果希望确保实参的值在函数中不应该被改变,这样可以加上 const 关键字
因此,写为,
void fun(const myClass & obj_){
// 函数中任何试图改变 obj 值都是变为非法的
}
2.6 类型转换构造函数
定义类型转换构造函数的目的是实现类型的自动转换。
只有一个参数,而且不会复制构造函数的构造函数,一般就可以看作是转换构造函数。
当需要的时候,编译系统会自动调用转换构造函数,建立一个无名的临时对象(或临时变量)。
#include <iostream>
using namespace std;
class Complex{
public:
double real, imag;
Complex(int i){
cout << "Int constructor called" << endl;
real = i; imag = 0;
}
Complex(double r, double i){real = r; imag = i;}
};
int main(){
Complex c1(7, 8);
cout << "-------------" << endl;
Complex c2 = 12; // int 类型
c1 = 9; // int 类型, 9 被自动转化成一个临时 complex 对象
cout << c1.real << "," << c1.imag << endl;
return 0;
}
结果如下,
2.7 析构函数
名字与类名相同,在前面加 ‘~’,没有参数和返回值,一个类最多只有一个析构函数。
析构函数对象消亡时自动被调用。可以定义析构函数来在对象消亡前做善后工作,比如释放分配的内存空间等。
如果定义类时没写析构函数,则编译器会自动生成缺省析构函数,缺省析构函数什么也不做。
/* 1. */
class String{
private:
char *p;
public:
String(){
p = new char[10];
}
~ String();
};
String::~ String(){
delete[] p;
}
/* 2. 析构函数和数组 */
class test{
public:
~test(){
cout << "destructor called" << endl;
}
};
int main(){
test arr[2];
cout << "End main" << endl;
return 0;
}
// 输出为:
End main
destructor called
destructor called
/* 3. 析构函数与运算符 delete */
test *pTest;
pTest = new test; // 构造函数被调用
delete pTest; // 析构函数被调用
-------------------
pTest = new test[3]; // 构造函数被调用 3 次
delete[] pTest; // 析构函数被调用 3 次
/* 4. 析构函数在对象作为函数返回值返回后被调用 */
class myClass{
public:
~myClass(){cout << "destructor" << endl;}
};
myClass obj;
myClass fun(myClass cobj){ // 参数对象消亡也会导致析构函数被调用
return cobj; // 函数调用返回时生成临时对象返回
}
int main(){
obj = func(obj); // 函数调用的返回值(临时对象)被
return 0; // 调用过后,该临时对象析构函数被调用
}
// 输出
destructor // 函数形参对象消亡
destructor // 临时对象消亡
destructor // 结束时全局对象消亡
2.8 构造函数和析构函数调用时机
构造函数和析构函数什么时候被调用,注意 new 出来的不 delete 不会消亡,可以这样理解,如下
#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); // 到main函数中
d4 = 6; // 自动转化为临时对象,执行完临时消亡
cout << "main" << endl;
{ Demo d5(5); // 局部对象,到包含最内存大括号消亡
}
Func();
cout << "main ends" << endl;
return 0;
}
结果如下,
2.8 this 指针
从 C++ 翻译到 C 语言部分理解,
//----------------- C++
class CCar{
public:
int price;
void setPrice(int p);
};
void CCar::setPrice(int p){price = p;}
int main(){
CCar car;
car.setPrice(20000);
return 0;
}
//------------------ C
struct CCar{
int price;
};
void setPrice(struct CCar *this, int p){
this -> price = p;
}
int main(){
struct CCar car;
setPrice(&car, 20000);
return 0;
}
this指针作用就是指向成员函数所作用的对象,举个例子,
从 C++ 翻译到 C 语言部分理解,
#include <iostream>
using namespace std;
// 非静态成员函数中可以直接使用 this 来代表指向该函数作用的对象的指针
class Complex{
public:
double real, imag;
void Print(){
cout << real << "," << imag;
}
Complex(double r, double i):real(r),imag(i){}
Complex addOne(){
this -> real++;
this -> Print();
return *this;
}
};
int main(){
Complex c1(1, 1), c2(0, 0);
c2 = c1.addOne(); // 输出 2,1
return 0;
}
注意:静态成员函数中不能使用 this 指针(静态成员函数并没有作用任何对象),因为静态成员函数并不具体作用与某个对象。因此,静态成员函数的真实参数的个数,就是程序中写出的参数个数。
2.9 静态成员变量和静态成员函数
静态成员,在说明前面加了 static 关键字的成员。
class Rectangle{
private:
int wid, high;
static int nTotalArea; // 静态成员变量
static int nTotalNum;
public:
Rectangle(int w_, int h_);
~Rectangle();
static void PrintTotal(); // 静态成员函数
};
普通的成员变量每个对象各自有一份,而静态成员变量一共就一份,为所有对象共享。sizeof 不会计算静态变量的大小 。
普通成员函数必须具体作用于某个对象,而静态成员函数并不具体作用与某个对象。因此静态成员不需要通过对象就能访问。
// 如何访问静态成员
1) 类名::成员名
Rectangle::PrintTotal();
2)对象名.成员名
Rectangle r; r.PrintTotal();
3) 指针->成员名
PrintTotal *p = &r; p -> PrintTotal();
4) 引用.成员名
PrintTotal &ref = r; int n = ref.nTotalNum;
静态成员变量本质上是全局变量,哪怕一个对象都不存在,类的静态成员变量也存在。
静态成员函数本质上也是全局函数。
设置静态成员这种机制的目的是将和某些类紧密相关的全局变量和函数写道类里面,看上去像一个整体,易于理解和维护。举个例子,
考虑一个需要随时知道矩形总数和总面积的图形处理程序。
可以用全局变量来记录总数和总面积
用静态成员将这两个变量封装进类中,就更容易理解和维护
如上面的程序,
#include <iostream>
using namespace std;
class Rectangle{
private:
int wid, high;
static int nTotalArea; // 静态成员变量
static int nTotalNum;
public:
Rectangle(int w_, int h_);
~Rectangle();
static void PrintTotal(); // 静态成员函数
};
Rectangle::Rectangle(int w_, int h_){
wid = w_; high = h_;
nTotalNum++;
nTotalArea += wid * high;
}
Rectangle::~Rectangle(){
nTotalNum--;
nTotalArea -= wid * high;
}
void Rectangle::PrintTotal(){
cout << nTotalNum << "," << nTotalArea << endl;
}
// 必须在定义类的文件中对静态成员变量进行一次说明或者初始化
// 否则编译不通过,链接不能通过
int Rectangle::nTotalNum = 0;
int Rectangle::nTotalArea = 0;
int main(){
Rectangle r1(3, 3), r2(2, 2);
// cout << Rectangle::nTotalNum;; // 错误的,私有
Rectangle::PrintTotal(); // 作用 r1 上面
r1.PrintTotal();
return 0;
}
结果如下,
在静态的成员函数中,不能访问非静态成员变量,静态成员函数也不能调用非静态成员函数。
void Rectangle::PrintTotal(){
cout << wid << "," << nTotalNum << "," << nTotalArea << endl;
// 错误的,wid 是非静态成员变量,PrintTotal 是静态成员函数
//
}
这样 Rectangle 类写法,有什么缺陷,
// 存在缺陷
Rectangle::Rectangle(int w_, int h_){
wid = w_; high = h_;
nTotalNum++; // 这部分
nTotalArea += wid * high;
}
Rectangle::~Rectangle(){
nTotalNum--; // 这部分
nTotalArea -= wid * high;
}
void Rectangle::PrintTotal(){
cout << nTotalNum << "," << nTotalArea << endl; // 这部分
}
// 在使用 Rectangle 类时,有时会调用复制构造函数生成临时的隐藏的 Rectangle 对象。
-- 》 调用一个以 Rectangle 类对象作为参数的函数时,
-- 》 调用一个以 Rectangle 类对象作为返回值的函数时,他们都是由复制构造函数初始化
// 临时对象(对象作为返回值,数字赋值给对象)在消亡时会调用析构函数,减少 nTotalNum 和 nTotalArea 的值,可是这些临时对象在生成时却没有增加他们的值。
// 解决方法:为 Rectangle类 写一个复制构造函数
Rectangle::Rectangle(Rectangle & r){
wid = r.wid; high = r.high;
nTotalNum ++;
nTotalArea += wid*high;
}
2.10 成员对象和封闭类
有成员对象的类(一个对象的成员,是其他对象的类)叫 封闭(enclosing )类,
#include <iostream>
using namespace std;
class Tyre{ // 轮胎类
private:
int radius;
int width;
public:
// 有初始化列表 radius = r; width = w;
Tyre(int r, int w):radius(r), width(w){}
};
class Engine{ // 引擎类
};
class Car{ // 汽车类 -- > 封闭类
private:
int price;
// 其他类作为成员变量,封闭类
Tyre tyre;
Engine engine;
public:
Car(int p, int tr, int tw);
};
Car::Car(int p, int tr, int w):price(p),tyre(tr, w){
};
int main(){
Car car(20000, 17, 225);
return 0;
}
上面例子中,如果 Car 类不定义构造函数,下面的语句会编译出错,
Car car;
因为编译器不知道 car.tyre 该如何初始化。 car.engine 的初始化就没问题,用默认构造函数就行。
任何生成封闭类对象的语句,都要让编译器明白,对象中的成员对象,是如何初始化的。具体做法就是通过封闭类的构造函数的初始化列表。
成员对象初始化列表的参数可以是任意复杂的表达式,可以包括函数,变量。只要表达式中的函数或变量有定义就行。
封闭类构造函数和析构函数的执行顺序,
1. 封闭类对象生成时,先执行所有对象成员的构造函数,然后才执行封闭类的构造函数;
2. 对象成员的构造函数调用次序和对象成员在类中的说明次序一致,与他们在成员初始化列表出现的次序无关;
3. 当封闭类的对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数。次序和构造函数的调用次序相反。
#include <iostream>
using namespace std;
class Tyre{ // 轮胎类
public:
Tyre(){cout << "Tyre constructor" << endl;}
~Tyre(){cout << "Tyre destructor" << endl;}
};
class Engine{ // 引擎类
public:
Engine(){cout << "Engine constructor" << endl;}
~Engine(){cout << "Engine destructor" << endl;}
};
class Car{ // 汽车类 -- > 封闭类
private:
// 其他类作为成员变量,封闭类
Tyre tyre;
Engine engine;
public:
Car(){cout << "Car constructor" << endl;}
~Car(){cout << "Car destructor" << endl;}
};
int main(){
Car car;
return 0;
}
结果如下
封闭类的复制构造函数,
#include <iostream>
using namespace std;
class temp{
public:
temp(){cout << "default" << endl;} // 构造函数
temp(temp & t){cout << "copy" << endl;} // 复制构造函数
};
class tempB{temp t;}; // 既有无参函数初始化又有复制构造函数初始化
int main(){
// b1.t() 初始化
// b2.t(b1.t) 初始化
tempB b1, b2(b1);
return 0;
}
// 输出
default
copy
----》 说明 b2.t 是用类 temp 的复制构造函数初始化的。
----》 而调用复制构造函数时的实参就是 b1.t.
2.11 常量对象,常量成员函数
如果不希望某个对象的值被改变,则定义该对象的时候可以在前面加 const 关键字。
#include <iostream>
using namespace std;
/* 1. */
// 1. 定义该对象的时候可以在前面加 const 关键字。
class Demo{
private:
int value;
public:
void setValue(){}
};
const Demo Obj; // 常量对象
/* 2. */
// 2. 在类的成员函数说明后面加上 const 关键字,则该成员函数成为 常量成员函数。
// 常量成员函数在执行期间不应该修改其所作用的对象。因此,在常量成员函数外不能修改成员变量的值(静态成员变量除外),也不能调用同类的非常量成员函数(静态成员函数除外)。
class Sample{
public:
int value;
void getValue() const;
void func(){};
Sample(){}
};
// 常量成员函数,参考上面
void Sample::getValue() const{
value = 0; // 错误的,在常量成员函数外不能修改成员变量的值
func(); // 错误的也不能调用同类的非常量成员函数
}
int main(){
const Sample o;
o.value = 100; // 错误的,常量对象不可修改
o.func(); // 错误的,常量对象上面不能执行非常量成员函数
o.getValue(); // 可以的,常量对象上可以执行常量成员函数
return 0;
}
常量成员函数的重载:两个成员函数,名字和参数表都一样,但是一个是 const,一个不是,算重载,
// 两个成员函数,名字和参数表都一样,但是一个是 const,一个不是,算重载
class Test{
private:
int n;
public:
Test() {n = 1;}
// 重载
int getValue() const {return n;}
int getValue(){return 2*n;}
};
int main(){
const Test obj1;
Test obj2;
cout << obj1.getValue() << "," << obj2.getValue();
return 0;
}
// 返回值:
1, 2
引用前面可以加 const 关键字,成为常引用。不能通过常引用修改其引用的变量。
const int & r = n;
r = 5; // 错误的
n = 4; // 正确的
对象作为函数参数时,生成该参数需要调用复制构造函数,效率比较低。用指针作参数,代码又不好看,如何解决,
// 可以用引用作为参数,如
class Sample{
};
void printObj(Sample & o){
};
对象引用作为函数的参数有一定的风险性,若函数中不小心修改了形参 o,则实参也跟着变,这可能不是我们想要的。如何避免,
// 可以用常引用作为参数,如
class Sample{
};
void printObj(const Sample & o){
};
这样的函数中,就能确保不会出现无意中更改 o 值的语句了。
2.12 友元(friends)
友元分为友元函数和友元类两种,
#include <iostream>
using namespace std;
/* 1. */
// 1. 友元函数:一个类的友元函数可以访问该类的私有成员
class Car; // 提前声明的 Car 类,以便后面的 CDriver 类使用
class CDriver{
public:
void modifyCar(Car *pCar); // 改装汽车
};
class Car{
private:
int price;
// 声明友元
friend int mostExpensiveCar(Car cars[], int total);
// 声明友元
friend void CDriver::modifyCar(Car *pCar);
};
// 友元可以直接使用
void CDriver::modifyCar(Car *pCar){
pCar->price += 1000; // 汽车改装后价值增加
}
// 友元可以直接使用
int mostExpensiveCar(Car cars[], int total){
int tempMax = -1;
for(int i = 0; i < total; ++i){
if(cars[i].price > tempMax)tempMax = cars[i].price;
}
return tempMax;
}
int main(){
return 0;
}
/* 2. */
// 2. 可以将一个类的成员函数(包括构造、析构函数)说明为另外一个类的友元
class B{
public:
void func();
};
class A{
friend void B::func();
};
/* 3. */
// 3. 友元类:如果 A 是 B 的友元类,那么 A 的成员函数可以访问 B 的私有成员
class Car{
private:
int price;
// 声明友元
friend class CDriver; //声明 CDriver 为友元类
};
class CDriver{
public:
Car myCar;
void modifyCar(){
// 因为 CDriver 是 Car 的友元类,可以访问其私有成员
myCar.price += 1000;
}
};
友元类的关系不能传递,不能继承。
2.13 运算符的重载
例如,两个复数是可以直接加减的,但是C++是不允许的。所以需要让对象也通过运算符运算复数。这样代码更简洁,容易理解。同一个运算符,对不同类型操作数,发生不同的行为。也可扩展到对象的使用。
运算符重载的实质是函数的重载;可以重载为普通函数,也可重载为成员函数;把含运算符的表达式转换成对运算符函数的调用;把运算符的操作数转换成运算符函数的参数;运算符被多次重载时,根据实参的类型决定调用哪个运算符函数。重载的形式
返回值类型 operator 运算符(形参表){
... ...
}
#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);
}
// 1. 重载为成员函数时,参数个数为运算符目数减 1 。
// 2. 重载为普通函数时,参数个数为运算符目数。
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-b).real << "," << (a-b).imag << endl;
// a - b 等价于 a.operator - (b)
return 0;
}
结果如下
2.14 赋值运算符重载
有时候希望赋值运算符两边的类型可以不匹配。赋值运算符 “ = ” 只能重载为成员函数。例如
#include <iostream>
using namespace std;
class String{
private:
char *str;
public:
String():str(new char[1]){str[0] = 0;}
const char * c_str(){return str;}
// str & 引用
String & operator = (const char *s);
String::~String(){delete[] str;}
};
String & String::operator = (const char *s){
// 重载 “=” 以使得 obj = “hello” 能够成立
delete[] str;
str = new char[strlen(s)+1];
strcpy(str, s);
return *this;
}
int main(){
String s;
s = "Gook Luck, "; // 等价于 s.operator = ("Gook Luck, ");
cout << s.c_str() << endl;
// String s2 = "hello!"; // 初始化语句,没有相应构造函数,不注释就会出错
s = "Shenzhou 8!"; // 等价于 s.operator = "Shenzhou 8!"
cout << s.c_str() << endl;
return 0;
}
输出结果如下,
浅拷贝和深拷贝,举例子如下,
#include <iostream>
using namespace std;
class String{
private:
char *str;
public:
String():str(new char[1]){str[0] = 0;}
// 为 String 类编写复制构造函数时,会面临和 = 同样的问题,用同样的方法处理
String(String & s){
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
}
const char * c_str(){return str;}
// str & 引用
String & operator = (const char *s){
delete[] str;
str = new char[strlen(s)+1];
strcpy(str, s);
return *this;
};
String & operator = (const String & s){
// 为了防止出现这样的情况 String s; s = "hello"; s = s(举例 a 可能引用到 s 的情况);
if(this == & s)return *this;
delete[] str;
str = new char[strlen(s.str)+1];
strcpy(str, s.str);
return *this;
}
~String(){delete[] str;}
};
int main(){
String S1, S2;
S1 = "this";
S2 = "that";
S1 = S2; // 程序崩溃,解释
// 相当于 S2 指向了 S1 ,留下了 this 空间,没有delete,
// 而且程序运行完以后,会 delete 两次 S2 地址部分(出现错误)
// 因此要加入一个 成员函数 如上
}
为什么上面返回的是 String & 类型。因为对运算符进行重载的时候,好的风格是应该尽量保留原运算符原本的特性。
可以返回 void 类型,也可以是 String 类型,为什么返回 String & 类型。
考虑
a = b = c;
(a = b) = c; // 会修改 a 的值
运算符重载为友元函数,
// 1. 一般情况下,将运算符重载了类的成员函数时,是较好的选择。
// 2. 但有时,重载为成员函数不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元。
class Complex{
double real, imag;
public:
Complex(double r, double i):real(r), imag(i){};
Complex operator + (double r);
friend Complex operator + (double r, const Complex & c);
};
// 满足 c = c + 5;
Complex operator + (double r){
// 返回一个临时对象
return Complex(real + r, imag);
}
// 满足 c = 5 + c, 但是不能访问 c 的私有成员,所以添加友元函数
Complex operator + (double r, const Complex & c){
// 返回一个临时对象
return Complex(c.real + r, c.imag);
}
int main(){
Complex c;
c = c + 5; // 有定义,相当于 c.operator + (5);
c = 5 + c; // 编译出错,可以把 + 重载为普通函数编译通过
// 如上
return 0;
}
2.15 可变长数组实现
比如要编写一个整型数组类,要满足main函数,
#include <iostream>
#include <cstring>
using namespace std;
class Array{
int size; // 数组元素个数
int *ptr; // 指向动态分配的数组
public:
Array(int s = 0); // s 代表数组元素个数
Array(Array & a);
~Array();
void push_back(int v); // 用于在数组尾部添加一个元素 v
// 用于数组对象间赋值
Array & operator=(const Array & a);
int length(){return size;} // 返回数组元素个数
// 返回值类型要考虑,非引用函数返回值不能作为左值使用
// 并且需要 4 能够修改数组元素相应位置元素,要用引用
// 返回值为 int 不行,不支持 a[i] = 4
int & Array::operator[](int i){
// 用以支持根据下标访问数组元素,
// 如 n = a[i] 和 a[i] = 4 这样的语句
return ptr[i];
}
};
Array::Array(int s):size(s){
if(s == 0)ptr = NULL;
else ptr = new int[s];
}
Array & Array::operator=(const Array & a){
// 赋值号的作用是使 “=” 左边对象里存放的数组,大小和右边的对象一样
if(ptr == a.ptr)return *this; // 防止 a = a 这样的赋值导致出错
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 Array::push_back(int v){ // 在数组尾部添加一个元素
if(ptr){
// 效率不太高
int *tempPtr = new int[size+1]; // 重新分配空间
memcpy(tempPtr, ptr, sizeof(int)*size); // 拷贝原数组内容
delete[] ptr;
ptr = tempPtr;
}
// 数组原来是空的,加入新的元素
else ptr = new int[1]; ptr[size++] = v;
}
// 如果不写复制构造函数,那么 a2.ptr 和 a1.ptr 都指向 a1.ptr
Array::Array(Array & 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;
}
Array::~Array(){
if(ptr)delete[] ptr;
}
int main(){ // 编写可变长整型数组类,使之能如下使用:
Array a; // 开始里的数组是空的
for(int i = 0; i < 5; i++){
a.push_back(i); // 动态内存分配存元素,需要指针变量
}
Array a2, a3;
a2 = a; // 需要重载“ = ”, 对象 a 赋给 a2
for(int i = 0; i < a.length(); i++){
cout << a2[i] << " "; // 重载“[ ]”运算符
}
a2 = a3; // a2 是空的
for(int i = 0; i < a2.length(); i++){
cout << a2[i] << " ";
}
cout << endl;
a[3] = 100; // 可变长
Array a4(a); // 复制构造函数
for(int i = 0; i < a4.length(); i++){
cout << a4[i] << " ";
}
return 0;
}
输出结果如下,
2.16 流插入运算符和流提取运算符的重载
cout << 5 << “this” 为什么能成立,cout 是什么,为什么 “<<” 可以用在cout上面。
cout 是 iostream 中定义的, ostream 类的对象。相当于对 “<<” 进行重载。考虑下面一个问题,
假如 c 是一个 Complex 复数类的对象,现在希望写 “cout << c;” ,就可以以 “a + bi” 形式输出 c 的值,写 “cin >> ;” ,就能够从键盘接受 “a + bi” 形式输入,并且使得 c.real = a, c.imag = b.
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
class Complex{
double real, imag;
public:
Complex(double r=0, double i=0):real(r),imag(i){};
friend iostream & operator<<(iostream & os,
const Complex & c);
friend istream & operator>>(istream & is,
const Complex & c);
};
iostream & operator<<(iostream & os, const Complex & c){
os << c.real << "+" << c.imag << "i"; // 以 “a+bi”形式输出
return os;
}
istream & operator>>(istream & is, const Complex & c){
string s;
is >> s; // 将“a+bi”作为字符串读入,“a+bi”之间不能有空格
int pos = s.find("+", 0);
string sTemp = s.substr(0, pos); // 分离出代表实部的字符
c.real = atof(sTemp.c_str()); // 字符转换为浮点数
sTemp = s.substr(pos+1, s.length()-pos-2); // 分离出虚部的字符
c.imag = atof(sTemp.c_str());
return is;
}
int main(){
Complex c;
int n;
cin >> c >> n;
cout << c << "," << n;
return 0;
// 要求输出结果为
// 输入 13.2 + 133i 87
// 输出 13.2 + 133i 87
}
这里没运行成功,以后理解看。
2.17 类型转换运算符重载
类型转换运算符也可以重载,如下,
#include <iostream>
using namespace std;
class Complex{
double real, imag;
public:
Complex(double r = 0, double i = 0):real(r),imag(i){};
operator double(){return real;}
// 重载强制类型转换运算符 double
};
int main(){
Complex c(1.2, 3.4);
cout << (double)c << endl; // 输出 1.2
double n = 2 + c; // 等价于 n = 2 + c.operator double()
cout << n; // 输出 3.2
}
2.18 自增,自减运算符的重载
自增运算符++,自减运算符–有前后置之分,为了区分所重载的是前置运算符还是后置运算符,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++也调用前置重载,而dev则令obj++编译出错。
举例如下,
#include <iostream>
using namespace std;
class Demo{
private:
int n;
public:
Demo (int i = 0):n(i){}
Demo & operator++();
Demo operator++(int);
operator int ( ) {return n;}
friend Demo & operator++(Demo &);
friend Demo operator--(Demo &, int);
};
Demo & Demo::operator++(){
// 前置++
++n;
return * this;
// ++s 即为:s.operator++();
}
Demo Demo::operator++(int k){
// 后置++
Demo temp(*this); // 记录修改前的对象
n++;
return temp; // 返回修改前的归降
// s++ 即为:s.operator++(0);
}
Demo & operator--(Demo & d){
// 前置--
d.n--;
return d;
// --s 即为:operator--(s);
}
Demo operator--(Demo & d, int){
// 后置--
Demo temp(d); // 记录修改前的对象
d.n--;
return temp;
// s-- 即为:operator--(s, 0);
}
int main(){
Demo d(5);
cout << (d++) << ","; // 等价于 d.operator++(0)
cout << d << ",";
cout << (++d) << ","; // 等价于 d.operator++()
cout << d << endl;
cout << (d--) << ","; // 等价于 d.operator--(d, 0)
cout << d << ",";
cout << (--d) << ","; // 等价于 d.operator--(d)
cout << d << endl;
return 0;
}
好像存在一些问题,没运行成功,以后有机会再回来理解。
三、总结
今天基本把 C++ 面向对象的一些语法学习了,后面学习面向对象的重要部分继承,多态,接口等知识,之前 JAVA 深入学习部分把这些过了一遍,后面学习 C++ 应该也不是特别难。同时也把 B 站上面小姐姐讲的计算机网络部分看到数据链路层部分。因为课程逻辑比较好,自己也在敲代码中学习。学习下来感觉不难,所以打算继续在应用中理解。