• C++类与对象


    学习资料:

    1.《C++面向对象程序设计教程(第三版)》 作者:陈维兴 林小茶 清华大学出版社

    2.《C++语言程序设计(第四版)》 作者:郑莉 董渊 何江舟 清华大学出版社

    面向对象程序设计的基本特点

    抽象

    抽象是指对具体问题(对象)进行概括,抽出一类对象的公共性质并加以描述的过程。
    抽象包括两个方面:数据抽象、行为抽象

    举例:
    对人进行抽象。

    数据抽象:姓名(string name)、性别(string sex)、年龄(int age)等。
    行为抽象:行走walk()、吃饭eat()、睡觉study()等。

    同一对象,由于侧重点不同可能会产生不同的抽象结果。

    封装

    封装就是将抽象得到的数据和行为相组合,形成一个有机的整体——类,其中数据和函数(行为/功能)都是类的成员。

    举例:
    将钟表封装成一个类。

    class Clock{
    	public: //外部接口,可在外部进行调用
    		void setTime(int newH,int newM,int newS);
    		void showTime();
    	private: //私有成员,只能被内部方法所访问,外部无法直接访问
    		int hour,minute,second;
    };
    

    封装使一部分成员作为外部接口,其他成员隐蔽起来,这样达到了对成员访问权限的合理控制,使不同类之间的相互影响减小到最低限度,增加数据的安全性。

    继承

    思考:
    虽然对人进行了封装,但是人与人之间又是不同的,比如学生有学号、年级班级等数据以及青年大学习等行为,而老师有工号、讲师/教授/博导等称号以及教书育人等行为。这些不同对象拥有特有的数据和行为。

    解决:
    C++提供一种类的继承的机制,允许程序员再保持原有类的特性的基础上,进行更具体、更详细的说明。

    多态

    例如我们日常生活中所说的打架、打篮球、打酱油,虽然都是“打”,但是含义不同,动作不同。这就是多态性。
    重载函数就是一种多态的手段。

    多态性是指一段程序能够处理多种类型对象的能力。

    C++中,这种多态性可通过强制多态、重载多态、类型参数化多态、包含多态4种形式实现。

    类和对象

    类是对象的抽象,对象是类的实例。

    类的声明

    先看一个栗子:
    下面是一个关于复数的类的定义:

    class Complex{ //关键字class声明名为Complex的类
    	double real; //数据成员,复数的实部
    	double imag; //数据成员,复数的虚部
    	
    	// 成员函数 给数据成员赋初值
    	void init(double r,double i){
    		real=r;
    		imag=i;
    	}
    	double abscomplex(){
    		double t;
    		t=real*real+imag*imag;
    		return sqrt(t);
    	}
    }; //注意这有个分号
    

    类是一种数据类型,它是用户定义的一种抽象的数据类型。

    编译下列代码:

    int main(){
    	Complex A; // 定义类的对象
    	A.init(1.1, 2.2); // 调用类的init方法赋初值
    	cout<<"复数的绝对值是:"<<A.abscomplex()<<endl;
    	return 0;
    }
    

    结果会出错:

    错误之处在于,init()和abscomplex()是私有的。也就是说,在不说明类的成员是public(公有)还是private(私有)时,C++默认时私有的,也就是说外部不能直接访问。
    我们也可以看出,如果一个类中全部都是私有成员,那么这个类是没有任何用处的。

    更改代码:

    ##include <iostream>
    ##include <cmath>
    using namespace std;
    
    class Complex{ // 关键字class声明名为Complex的类
    	private: //声明以下成员为私有
        		double real; //数据成员,复数的实部
        		double imag; //数据成员,复数的虚部
        
        public: // 声明以下成员为共有
        		// 成员函数 给数据成员赋初值
        		void init(double r,double i){
        	   		real=r;
        	   		imag=i;
        		}
        		double abscomplex(){
            		double t;
            		t=real*real+imag*imag;
            		return sqrt(t);
        		}
    }; //注意这有个分号
    
    int main(){
        Complex A; // 定义类的对象
        A.init(1.1, 2.2); // 调用类的init方法赋初值
        cout<<"复数的绝对值是:"<<A.abscomplex()<<endl; // 调用abscomplex()方法求绝对值
        return 0;
    } 
    

    这样,外部就可以通过共有的方法简洁的访问类的成员。使得类内的私有成员变得更加安全。

    运行结果:

    说明:
    protected也是类成员的一个关键字,说明被保护成员,可由本类和派生类内的成员函数访问,其他访问是非法的。它是半隐蔽的。

    成员函数的定义

    一般函数成员是公有的,习惯把共有成员写在前面,这样便于阅读。

    在类内部定义函数(隐式声明)

    例如2.1中的例子一样定义函数就可以了。
    这种定义方式,C++编译器将函数作为内联函数处理。

    在类外部定义函数

    首先要在类中声明函数原型,然后再在外部定义函数。

    一般形式:

    返回值类型 类名::成员函数名(参数表)
    {
    	函数体
    }
    

    栗子:

    class Point{
    	public:
    		void setpoint(int, int); // 声明成员函数
    		int getx(); // 声明函数
    		int gety(); // 声明函数
    	private:
    		int x,y;
    };
    int Point::getx(){
    	return x;
    }
    int Point::gety(){
    	return y;
    }
    void Point::setpoint(int a, int b){
    	x=a;
    	y=b;
    }
    

    注意:Point::是必须写的。

    采用这种定义方式是C++良好的类定义习惯,它可以减少类定义的长度,使阅读起来比较方便。

    函数的显示定义

    就是在外部定义函数时,在函数最前面用inline声明成内联函数。

    inline int Point::getx(){
    	return x;
    }
    

    对象

    对象的定义

    定义一般形式:

    类名 对象1, 对象2, 对象3, ……;
    
    Point A,B;
    

    类是一个模板,他不接收和存储数据,定义对象后,系统才会为对象分配对应的存储空间,用来存放对象总的成员。

    对象中成员的访问

    通过对象选择符选择访问对象中的成员

    一般形式:
    对象名.数据成员

    对象名.成员函数(实参表)

    Point op1;
    op1.setpoint(1,2); // 调用对象op1的成员函数给op1的数据成员赋值
    int i = op1.getx(); // 调用对象op1的成员函数取得x的值
    
    通过指向对象的指针访问对象中的成员

    我们可以定义一个指针指向这个对象,则访问时使用->操作符:

    class Date{
        public:
            int year;
    };
    
    int main(){
        Date d, *p; // 定义了一个Date的对象的指针
        p = &d; // p指向对象d
        cout<< p->year << endl;// 使用->访问成员year
        cout<< (*p).year << endl; // 两者等价,因为*p就是对象d
    }
    
    通过引用访问对象成员
    class Date{
    	public:
    		int year;
    };
    
    int main(){
    	Date d;
    	Date &dd = d; // 定义d的引用
    	cout<<d.year;
    	cout<<dd.year;
    }
    

    dd是d的引用,所以dd与d占有相同的存储单元。

    构造函数与析构函数

    在定义对象时,有时需要对数据成员进行初始化,这个任务由构造函数完成。

    在特定对象使用结束后,还经常需要进行一些清理工作,这个任务由析构函数完成。

    对象的初始化与构造函数

    类是一种抽象的数据类型,它不占存储空间不能容纳具体数据,所以不能给数据成员赋初值

    所以下列程序是错误的。

    class Date{
        public:
            int year = 2019;
    };
    

    DEV-C++编译会出警告但是能运行,但是这种写法是错误的,最好不要使用,编程尽量按照规矩来,避免不同环境带来的奇怪的报错。

    如果类中的所有成员都是共有的,则可以在定义对象时初始化数据成员。

    class Complex{
    	public:
    		double real;
    		double imag;
    };
    Complex c = {1.1, 2.2};
    

    如果成员是私有的,可以利用公有函数对对象中的数据成员赋初值:

    class Complex{
    	public:
    		void init(double ,double);
    		double abscomplex();
    	private:
    		double real;
    		double imag;
    };
    void Complex::init(double a, duuble b){
    	real = a;
    	imag = b;
    }
    double Complex::abscomplex(){
    	return sqrt(real*real + imag*imag);
    }
    int main(){
    	Complex c;
    	c.init(3.0, 4.0);
    	cout<<c.abscomplex();
    }
    
    // 运行结果:5
    

    使用成员函数,给数据成员赋值,既不方便记忆也容易忘记,甚至会出错。所以,C++提供了构造函数来完成对象的初始化。

    构造函数有以下特点:

    • 必须与类同名
    • 可以有任意类型的参数
    • 不能具有返回值类型,void也不行
    • 建立对象时自动执行

    下面定义一个构造函数:

    class Complex{
        public:
        	Complex(double r, double i){ // 析构函数 
        		real = r;
        		imag = i;
    		}
            double abscomplex();
        private:
            double real;
            double imag;
    };
    double Complex::abscomplex(){
        return sqrt(real*real + imag*imag);
    }
    int main(){
        Complex c(3.0, 4.0); // 形式一
        cout<< c.abscomplex() << endl;
        Complex *p = new Complex(8.0,6.0); // 形式二
        cout<< p->abscomplex() <<endl;
    }
    /*
    运行结果:
    5
    10
    */
    

    将构造函数放在类外部定义:

    class Complex{
        public:
        	Complex(double r, double i);
            double abscomplex();
        private:
            double real;
            double imag;
    };
    // 外部定义构造函数
    Complex::Complex(double r, double i){
    	real = r;
    	imag = i;
    }
    double Complex::abscomplex(){
        return sqrt(real*real + imag*imag);
    }
    int main(){
        Complex c(3.0, 4.0);
        cout<< c.abscomplex() << endl;
    }
    // 运行结构:5
    

    在对构造函数做以下几点说明:

    构造函数里面可以定义与初始化无关的操作,但是不提倡加入与初始化无关的操作。

    构造函数一般声明为公有成员,但是它是在定义对象是自动调用的,而且只执行一次。像c.Complex(2.1,5.4)这种操作是非法的、错误的。

    实际应用中,通常需要给每一个类都定义构造函数,如果没有定义的话,系统则会自动生成一个构造函数:Complex::Complex(){}

    可以没有形参。例如:Complex(){real = 0; image = 0;}。此时定义对象时Complex c,构造函数会被自动执行。

    用成员初始化列表对数据成员初始化

    构造函数的另一种写法和调用方法。
    一般形式:

    构造函数名(形参表):数据成员1(初始化值1),数据成员2(初始化值2),……
    {
    	构造函数体
    }
    
    Complex(double r, double i):real(r), imag(i)
    {}
    

    这种方法与普通的方法是有所区别的。
    普通构造函数是初始化数据成员是一种使用=赋值操作,但是在C++中有些数据成员是不允许直接赋值的,例如const修饰的数据成员,或者是引用类型的数据成员,因此只能使用成员初始化列表对其初始化:

    class A{
    	public:
    		A(int); // 声明原型函数 
    		void print(); 
    	public:
    		int x;
    		int &rx;
    		const double pi;	
    }; 
    A::A(int xx):x(xx), rx(x), pi(3.14)
    {} // 注意这里有一对花括号 
    void A::print(){
    	cout<<"x="<<x<<endl;
    	cout<<"rx="<<rx<<endl;
    	cout<<"pi="<<pi<<endl;
    }
    int main(){
    	A a(7);
    	a.print();
    	return 0;
    }
    /*
    运行结果:
    x=7
    rx=7
    pi=3.14
    */
    

    注意:使用该种方法初始化数据成员时,初始化顺序与成员初始化列表是没有关系的,是按照数据成员在类中声明的顺序初始化的。

    class A{
    	public:
    		A(int); // 声明原型函数 
    		void print(); 
    	public:
    		int x;
    		int y;	
    }; 
    A::A(int xx):y(2*x),x(xx)
    {} // 注意这里有一对花括号 
    void A::print(){
    	cout<<"x="<<x<<endl;
    	cout<<"y="<<y<<endl;
    }
    int main(){
    	A a(7);
    	a.print();
    	return 0;
    }
    /*
    运行结果:
    x=7
    y=14
    */
    

    建议使用成员初始化列表初始化数据成员,方便、简练。

    构造函数的重载

    在C++,函数的重载是非常实用的,很多时候我们对不同的类型的数据进行相同的操作,就会用到重载。构造函数同样可以重载。

    ##include <iostream>
    ##include <cmath>
    using namespace std;
    
    class Date{
    	public:
    		Date();
    		Date(int a, int b, int c);
    		void print();
    	private:
    		int year, month, day;
    };
     
    Date::Date():year(2019), month(12), day(3){} // 初始化成默认数据
    Date::Date(int a, int b, int c):year(a), month(b), day(c){}
    void Date::print(){
    	cout<<year<<"年"<<month<<"月"<<day<<"日"<<endl; 
    }
    
    int main(){
    	Date d1(2018, 12, 3);
    	cout<<"d1: ";
    	d1.print();
    	
    	Date d2; // 没有参数,初始成默认数据 
    	cout<<"d2: ";
    	d2.print();
    }
    /*
    运行结果:
    d1: 2018年12月3日
    d2: 2019年12月3日
    */
    

    尽管一个类可以定义多个构造函数,但是创建一个对象时只会调用其中一个构造函数

    带默认参数的构造函数

    构造函数允许带有默认参数,当创建对象时没有参数的传递就会使用默认参数。

    class Date{
    	public:
    		Date(int a=2018, int b=12, int c=3); //指定默认参数 
    		void print();
    	private:
    		int year, month, day;
    };
    
    // 在类外部创建构造函数时可以不再次指定默认参数值
    Date::Date(int a, int b, int c):year(a), month(b), day(c){}
    void Date::print(){
    	cout<<year<<"年"<<month<<"月"<<day<<"日"<<endl; 
    }
    
    int main(){
    	Date d1(2019); // 调用时可以只指定部分参数
    	cout<<"d1: ";
    	d1.print();
    	
    	Date d2; // 没有参数,使用默认参数 
    	cout<<"d2: ";
    	d2.print();
    }
    /*
    运行结果:
    d1: 2019年12月3日
    d2: 2018年12月3日
    */
    

    说明:

    一个类中如果定义了一个带有默认参数的构造函数,就不能再定义别的带有默认参数的构造函数或者是不带参数的构造函数。例如:
    Complex(double r=1.1, double i=2.2);
    Complex();
    如果存在这样两个构造函数的话,编译系统则不知道该调用哪个构造函数。

    如下情况编译系统也无法判断应该调用哪个函数:
    Complex(double r=1.1, double i=2.2);
    Complex(double r);

    析构函数

    析构函数执行与构造函数相反的操纵,通常用来执行一些清洗工作,如释放分配给对象的空间等。
    它有以下特点:

    • 析构函数名与类名相同
    • 析构函数不具有任何返回值,不能说明它的类型
    • 析构函数没有参数,不能重载
    • 撤销对象时,系统自动调用析构函数
    class Date{
    	public:
    		Date(int a=2018, int b=12, int c=3); //带有默认参数 
    		~Date(); //声明析构函数 
    		void print();
    	private:
    		int year, month, day;
    };
    
    Date::~Date(){ // 析构函数 内部可以写程序体 
    	cout<<"清洗完毕"<<endl; 
    }
    Date::Date(int a, int b, int c):year(a), month(b), day(c){}
    void Date::print(){
    	cout<<year<<"年"<<month<<"月"<<day<<"日"<<endl; 
    }
    
    int main(){
    	Date d1(2019);
    	cout<<"d1: ";
    	d1.print();
    	
    	Date d2; // 没有参数,使用默认参数 
    	cout<<"d2: ";
    	d2.print();
    }
    /*
    运行结果:
    d1: 2019年12月3日
    d2: 2018年12月3日
    清洗完毕
    清洗完毕
    */
    

    可以看出,当对象的生命周期结束时,系统将自动调用析构函数,清洗对象的存储空间。

    如果没有定义析构函数,系统会默认定义Date::~Date(){}作为类的析构函数。一般情况下系统自动定义的析构函数足以满足需要。

    但是,当在清理对象之前还需要做一些操作的话,则需要自己定义析构函数。

  • 相关阅读:
    [转] 礼物的含义
    刘邦与项羽的关系
    inner join...on和join...on用法
    Oracl常用到问题
    [转] 河南、的女孩好…
    人的痛苦往往在于追求的不是“幸福”,而是“比别人幸福”!
    oracle 基础学习
    WORD文档中的回车符和空格键符如何去掉?
    采用FireFox浏览器进行页面级开发
    Windows Socket Programming 网络编程系列 简单客户端与服务器
  • 原文地址:https://www.cnblogs.com/xxmmqg/p/12901482.html
Copyright © 2020-2023  润新知