本文是“北京大学—程序设计实习[C++]”公开课的听课笔记,配套教材是任课老师自己编写的《新标准C++程序设计教程》,博主是入门菜鸟,故整理并记录在博客里,方便后续查看。

浅谈面向对象程序设计方法

结构化程序设计存在的问题

结构化程序设计:复杂的大问题->层层分解/模块化->若干子问题

自顶向下,逐步求精

程序=数据结构+算法

存在的问题:理解难,修改难,查错难,重用难;

面向对象的程序设计

面向对象的程序=类+类+...+类

面向对象的程序设计方法:

面向对象的程序设计方法

类的定义

class 类名
{
    访问范围说明符:
        成员变量1
        成员变量2
        ……
        成员函数声明1
        成员函数声明2
        ……
    访问范围说明符:
        更多成员变量
        更多成员函数声明
        ……
}

面向对象程序设计语言的发展历程

2011年,提出了c++11标准

从客观事物抽象出类的例子

例:写一个程序,输入矩形的宽和高,输出面积和周长。

  • 矩形的属性:宽和高
    • 两个变量,分别代表宽和高
  • 对矩形的操作
    • 设置宽和高
    • 计算面积
    • 计算周长

客观事物->类

矩形类:成员变量+成员函数(可以理解成一个带函数的结构体)

#include <stdio.h>
#include <iostream>

using namespace std;

class CRectangle {
public:
    int w, h;
    void Init(int w_, int h_) {
        w = w_; h = h_;
    }
    int Area() {
        return w * h;
    }
    int Perimeter() {
        return 2 * (w + h);
    }
};//必须有分号

int main() {
    int w, h;
    CRectangle r;//r是一个对象
    cin >> w >> h;
    r.Init(w, h);
    cout << "the area is " << r.Area() << endl << "the perimeter is " << r.Perimeter();
    return 0;
}

类定义的变量--类的实例--“对象”

对象的内存分配

对象的内存空间

  • 对象的大小 = 所有成员变量的大小之和
  • E.g. CRectangle 类的对象,sizeof(Crectangle) = 8,成员变量是两个int类型的,一个int型占4个字节。

每个对象各有自己的存储空间,一个对象的某个成员变量被改变,不会影响到其他的对象。

对象间的运算

对象之间可以用“=”进行赋值

不能用“==”,“!=”,“<”,“>”,“<=”,“>=”进行比较,除非这些运算符经过了“重载”。

访问类的成员变量和成员函数

用法1:对象名.成员名

Crectangle r1, r2;
r1.w = 5;
r2.Init(3,4);

用法2:指针->成员名

CRectangle r1, r2;
CRectangle * p1 = & r1;
CRectangle * p2 = & r2;
p1->w = 5;
p2->Init(3,4);//Init作用在p2指向的对象上

用法3:引用名.成员名

CRectangle r2;
CRectangle & rr = r2;
rr.w = 5;
rr.Init(3,4);//rr的值变了,r2的值也变了

另一种输出结果的方式

void PrintRectangle(CRectangle & r){
    cout << r.Area()<<","<<r.Perimeter();
}
CRectangle r3;
r3.Init(3,4);
PrintRectangle(r3);

类的成员函数的另一种写法

成员函数体和类的定义分开写,使用::区分

class CRectangle {
pubilc:
	int w, h;
	int Area();//成员函数在此处声明
	int Perimeter();
	void Init(int w_, int h_);
};//一定要有分号

int CRectangle::Area() {
	return w * h;
}

int CRectangle::Perimeter() {
	return 2 * (w + h);
}

void CRectangle::Init(int w_, int h_) {
	w = w_; h = h_;
}

调用时通过:对象、对象的指针、对象的引用

类成员的可访问范围

关键字:类成员可被访问的范围,说明类成员的可见性,缺省时为私有成员。

  • private:指定私有成员,只能在成员函数内被访问
  • public:指定公有成员,可以在任何地方被访问
  • protected:指定保护成员,保护成员可以被派生类的成员函数引用

三种关键字出现的次数和先后次序都没有限制

对象成员的访问权限

定义一个类:

class className{
    private:
    	私有属性和函数
    public:
    	公有属性和函数
    protected:
    	保护属性和函数
}

举例:

class Man{
    	int nAge;//私有成员
    	char szName[20];
    public:
    	void SetName(char * Name){
            strcpy(szName, Name);
        }
};

类的成员函数内部,可以访问:

  • 当前对象的全部属性,函数
  • 同类其他对象的全部属性,函数

类的成员函数以外的地方,可以访问:

  • 只能够访问该类对象的公有成员(public)
#include <stdio.h>
#include <string.h>
#include <iostream>

using namespace std;

class CEmployee {
private:
	char szName[30];//名字
public:
	int salary;//工资
	void setName(char* name);
	void getName(char* name);
	void averageSalary(CEmployee e1, CEmployee e2);
};

void CEmployee::setName(char* name) {
	strcpy(szName, name);//ok
}

void CEmployee::getName(char* name) {
	strcpy(name, szName);//ok
}

void CEmployee::averageSalary(CEmployee e1, CEmployee e2) {
	salary = (e1.salary + e2.salary) / 2;
}

int main() {
	CEmployee e;
	char mName[30];//
	//strcpy(e.szName,"Tom123456789");//编译错,不能访问私有成员
	e.setName("Tom");//ok
	e.salary = 5000;//ok
	cout << e.salary << endl;
	e.getName(mName);
	cout << mName << endl;//输出员工人名
	return 0;

程序中必须使用接口函数(setName)对私有变量进行修改。

设置私有成员的目的

强制对成员变量的访问一定要通过成员函数进行

设置私有成员的机制:

隐藏

程序容易修改:

如szName->char szName[5]

如果szName不是私有,需要修改全部:

strcpy(man1.szName,"Tom24325435366");

如果将szName变为私有,所有对szName的访问都是通过成员函数来进行:

man1.setName("Tom1234543534635645");

内联成员函数与重载成员函数

内联成员函数

减少函数调用的开销,内联成员函数的定义方式

  • inline+成员函数
  • 整个函数体出现在类定义内部
class B {
	inline void func1();
	void func2() {
		...
	};
};
void B::func1(){}

成员函数的重载及参数缺省

重载成员函数

成员函数:带缺省参数

#include <iostream>

using namespace std;

class Location {
private:
	int x, y;
public:
	void init(int x = 0, int y = 0);
	void valueX(int val) {
		x = val;
	}
	int valueX() {
		return x;
	}
};

void Location::init(int X, int Y) {
	x = X;
	y = Y;
}

int main() {
	Location A;
	A.init(5);//以5,0初始化
	A.valueX(5);//将x设为5
	cout << A.valueX();//重载成员函数,输出5
	return 0;
}

使用缺省参数的时候,要注意避免函数重载时的二义性

class Location {
private:
	int x, y;
public:
	void init(int x = 0, int y = 0);
	void valueX(int val = 0) {
		x = val;
	}
	int valueX() {
		return x;
	}
};

Location A;
A.valueX();//error,编译器无法判断调用哪个valueX

构造函数

基本概念(P179)

  • 成员函数的一种
    • 名字与类名相同,可以有参数,不能有返回值(void也不行)
    • 作用是对对象进行初始化,如给成员变量赋初值
    • 如果定义类时没写构造函数,则编译器生成一个默认的无参数的构造函数
      • 默认构造函数无参数,不做任何操作
  • 如果定义了构造函数,则编译器不生成默认的无参数的构造函数
  • 对象生成时构造函数自动被调用。对象一旦生成,就再也不能在其上执行构造函数
  • 一个类可以有多个构造函数

为什么需要构造函数

1) 构造函数执行必要的初始化工作,有了构造函数,就不必专门再写初始化函数,也不用担心忘记调用初始化函数。

2)有事对象没被初始化就使用,会导致程序错误。

class Complex {
private:
	double real, imag;
public:
	void set(double r, double i);
};//编译器自动生成默认构造函数

Complex c1;//默认构造函数被调用
Complex* pc = new Complex;//默认构造函数被调用

有构造函数的情况:

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 cl(2);//ok
Complex c1(2, 4), c2(3, 5);
Complex* pc = new Complex(3, 4);

上面程序中,new出来的对象返回值是指针。

可以有多个构造函数,参数个数或类型不同:

class Complex {
private:
	double real, imag;
public:
	void Set(double r, double i);
	Complex(double r, double i);
	Complex(double r);
	Complex(Complex c1, Complex c2);
};

Complex::Complex(double r, double i) {
	real = r;
	imag = i;
}

Complex::Complex(double r) {
	real = r;
	imag = 0;
}

Complex::Complex(Complex c1, Complex c2) {
	real = cl.real + c2.real;
	imag = c1.imag + c2.imag;
}

Complex c1(3), c2(1, 0), c3(c1, c2);
//c1 = {3,0}, c2 = {1,0}, c3 = {4,0}

上面程序中,整型可以自动被转换为double类型。

构造函数在数组中的使用

#include <iostream>

using namespace std;

class CSample {
private:
	int x;
public:
	CSample(){
		cout << "Constructor1 Called" << endl;
	}
	CSample(int n) {
		x = n;
		cout << "Constructor2 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;
}
//输出:
//Constructor1 Called
//Constructor1 Called
//step1
//Constructor2 Called
//Constructor2 Called
//step2
//Constructor2 Called
//Constructor1 Called
//step3
//Constructor1 Called
//Constructor1 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 array3[3] = {Test(2,3),Test(1,2),1};
//三个元素分别用(2),(2),(1)初始化
Test* pArray[3] = {new Test(4), new Test(1,2)};
//两个元素分别用(1),(2)初始化,pArray[2]未初始化,是一个未经初始化的指针

Test* pArray[3]是一个指针数组,不是对象数组,里面每个元素是个指针,不是对象,不会引发对象的生成。

new出来的对象返回值是指针,Test* pArray[3]数组里的指针指向new出来的对象,pArray[0]和pArray[1]分别指向用(1),(2)初始化的对象,pArray[2]是一个未经初始化的指针