从C到C++

第2章 类和对象

2016-06-08  本文已影响52人  笑笑学生

2.1 类的基础知识
2.2 构造函数
2.3 拷贝构造函数
2.4 析构函数
2.5 C++能自动产生成员函数
2.6 友元
2.7 类成员的补充
2.8 简单内存对象模型

2.1 类的基础知识

面向对象基本概念

类和对象
类是一组具有相同属性和行为的对象的抽象。类和对象的关系是抽象和具体的关系。
封装、继承和多态

  1. 用类进行封装
  2. 继承:保持已有类的特性而构造新类的过程。
  3. 多态:使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。

抽象和封装
在 C++ 中,用类来定义自己的抽象数据类型
通过定义类型来对应所要解决的问题中的各种概念,可以使我们更容易编写、调试和修改程序。
class Clock{
public:
void SetTime(int SetHour, int SetMinute);
void ShowTime();
private:
int m_Hour; int m_Minute
};
类是有着共同特征与行为、而状态各不相同的物体的总称。
对象是类的实现,是类的实例。类的产生基础是封装。

封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只需要通过外部接口,以特定的访问权限使用其中的成员即可。
C++支持数据隐藏机制,public,protected, private成为保护标识符,标识后边成员的可访问性。

成员函数

可以仅在类中说明原型,而在类外给出函数体实现,也可以直接在类中给出函数体,形成成员函数的隐含内联。
允许成员函数为重载函数和带默认参数值的函数。
对成员数据的使用不再遵循“先声明后使用”的原则。
凡被调用的成员函数一定要有函数实现。

:: ---- 作用域操作符,类就是定义了一个新的作用域。
void Clock::SetTime(int SetHour, int SetMinute)
{
m_Hour = SetHour;
m_Minute = SetMinute;
}
内联显示声明:
inline int Point::GetX() {
return X;
}

创建对象

对象就是类的实体,就是把抽象出的数据具体化。类就是一个封装了数据和函数的类型。
添加头文件#include "clock.h"
Clock myclock; //创建对象,就像是创建一个基本类型
myclock.SetTime(12, 30); //对象名.成员

一般来说类的定义与类中成员函数的定义并不放在同一个文件中。类的声明放在头文件中,类中函数的定义(类实现及类使用)放在.cpp文件中

类的深入

类:1、把数据封装(使用private),维护方便。2、使程序更加模块化,逻辑更清晰。
类用户的代码中永远不会出现private封装起来的内容。
Class默认为私有,struct默认为公有

类作用域
类定义部分 {}
类外成员函数形参表部分
类外成员函数函数体部分

成员函数的返回类型不一定在类作用域中
类内定义typedef int HOUR; 类外使用HOUR Clock::GetHour(){ }编译错误。

  1. 作用域是对象或成员能被使用的前提,表现了“可见性”。是语法规定好的,程序员只能遵照执行。
    2.访问权限是类的对象访问成员的被许可性,也是类成员的可用性。表现了“封装性”。是在作用域的前提下,由程序员定义的,是人的意志的体现

类内访问 和 类外访问
类作用域内的访问为“类内访问”,类作用域外的访问为“类外访问”
类内访问:
void Clock::SetTime(int newHour, int newMinute){
m_Hour = newHour;
m_Minute = newMinute;
}

拷贝构造函数:

Clock(const Clock& c1): m_Hour(c1.m_Hour),m_Minute(c1.m_Minute) {
cout<<"拷贝构造函数调用"<<endl;
}
X(p.X)是当前对象的函数访问别的对象的私有数据,这不是破坏了封装性吗?
不。这是发生在成员函数中,叫“类内访问”:成员函数和私有数据都在同一个类中。而不属于“对象名.成员名”的表示,那是“类外访问”。

隐含的this指针

void Clock::SetTime( int SetHour, int SetMinute)
{
this->m_Hour = SetHour;
this->m_Minute = SetMinute;
}
this指针是什么,在面向对象编程中,针对结构体,会有相关的成员函数,而成员函数的第一个参数通常就是本类对象的指针,既然这是一个固定的方法,C++索性就把它给封装在后边。本类对象的所有代码是公共的。
每一个成员函数(static member function除外)都有一个隐含的参数,为 classType* this,会自动加载进去。
SetTime( Clock* this, int SetHour, int SetMinute)


2.2 构造函数

定义及作用

构造函数是特殊的成员函数,通常创建类类型的新对象,都要执行构造函数。
构造函数的工作是保证每个对象的数据成员具有合适的初始值
构造函数的作用是在对象被创建时使用特定的值构造对象,或者说将对象初始化为一个特定的状态。
特殊在哪儿?1.不能由对象调用,自动调用!2.没有返回值。3.有初始化列表

初始化:是指对象(变量)诞生的那一刻即得到原始值的过程。是动词。
若对象声明然后再赋值,会经历状态由不确定到确定的过程。
调用时机
当然是在任何构造对象的时候:
a) 无论这个对象有名无名.
b) 无论这个对象被创建在什么区域.
c) 无论这个对象是否为临时对象.
Clock clock;
Clock *pClock = new Clock;

如果程序中未声明,则系统自动产生出一个默认形式的构造函数,它无参,函数体为空;
允许为内联函数、重载函数、带默认形参值的函数
是可以使用初始化列表的两函数之一(特权(另一个拷贝构造函数));
不可以是常函数,亦不可是虚函数;
天然具有类型转换功能,除非用explicit关闭。

语法规则及特殊性体现

类名::类名(形参总表):成员或对象1(参数1),成员或对象2(参数2)
{
构造函数的函数体
}

Clock::Clock(int h, int m):m_Hour(h), m_Minute(m)
{
}
构造函数名与类名相同 //一个类可以有多个构造函数
不可有任何返回值
可以有初始化列表(只有构造函数有初始化列表)

初始化列表

Clock::Clock(int h, int m) 这里是初始化的时机
{
//如果放弃了初始化的时机,那么就要在这里进行数据成员的赋值
m_Hour = h;
m_Minute = m;
//m_serial_number = “CASIO-EDIFICE-2009001”;
}

注意,无论是否写初始化列表,在进入函数体之前,都有一个初始化的机会。
这个机会留给你初始化特殊的成员变量,如引用,const成员
例如: const string m_serial_number;

倔强变量需要初始化列表
const变量
引用
IO流对象 //你可以暂且先记住它
所以,如果类内包含这三类成员,一定要使用初始化列表对他们进行初始化工作。
建议使用初始化列表给成员初始化,因为效率比赋值高

默认构造函数
A a; //类名 对象名; 不带任何初值
那么编译器为你调用的构造函数就是默认构造函数。

构造对象的方式
有名对象:A a 或A a(参数)
无名对象(堆): new A 或 new A(参数)
注:A a;千万不可写成 A a();

规范:初始化列表一定要按照成员变量的声明次序书写,以免引发隐晦的错误。
A() { j = 10; i = j/2;} // A():j(10), i(j/2) {}


2.3 拷贝构造函数

使用一个已经存在的对象去初始化同类的一个新对象时调用拷贝构造函数
拷贝构造函数是一种特殊的构造函数,其形参为本类的对象引用。
规范,建议使用const引用

语法规则
class 类名
{
public:
类名(形参);//构造函数
类名(const 类名 &参数名);//拷贝构造函数原型
};
类名::类名(const 类名 &参数名) //拷贝函数的实现
{
函数体
}
Clock(const Clock& c1): m_Hour(c1.m_Hour),m_Minute(c1.m_Minute) {
cout<<"拷贝构造函数调用"<<endl;
}
调用方式
当用类的一个对象去初始化该类的另一个对象时系统自动调用拷贝构造函数实现拷贝赋值。
void main(void)
{
Clock c1(1,2);
Clock c2(c1); //拷贝构造函数被调用
}
若函数的形参为类对象,调用函数时,实参赋值给形参,系统自动调用拷贝构造函数
void fun1(Clock c)
{
cout<<c.GetHour()<<endl;
}
int main()
{
Clock c1(1,2);
fun1(c1);
return 0;
}
当函数的返回值是类对象时,系统自动调用拷贝构造函数。
Clock fun2(){
Clock c(1,2);
//调用拷贝构造函数
return c;
}
void main()
{
Clock c1;
c1 = fun2(); //临时对象
}


2.4 析构函数

定义:(与构造函数相对立)
用来完成对象被删除前的一些清理工作。
析构函数是在对象的生存期即将结束的时刻被自动调用的
语法规则:
类名::~类名()
{
函数体
}
注:
函数名为 ~类名
函数没有参数
函数体负责资源清理工作(内存)

为什么需要析构函数?
处理僵死进程。
Array(int size):m_Size(size) { m_pArray = new int[m_Size]; }
~Array(){
delete [] m_pArray;
}
析构函数的调用顺序与构造函数的调用顺序严格相反
发生了2次析构函数的调用,用new分配空间,需要在delete的时候才会调用析构函数
int main()
{
Test a;
{
Test *pTest = new Test;
Test b;
delete pTest;
pTest = NULL;
}
return 0;
}

MyString类函数练习

错误调试:注意赋值与等于符号、注意动态申请的内存delete

#include <iostream>
#include <cstring>

using namespace std;

class MyString {
public:
    MyString(char *pstr="");
    MyString(const MyString& str);
    ~MyString();
public:
    void ShowString();  //显示字符串
    void Add(const MyString& str);  //与另外一个相加
    void Copy(const MyString& str); //从另外一个拷贝
    int GetLength() const;
private:
    char *m_buf;
};
MyString::MyString( char *pstr)
{
    if(pstr)
    {
        m_buf = new char[strlen(pstr) +1];
        strcpy(m_buf, pstr);
    }
    else
    {
        m_buf = new char[1];
        *m_buf = '\0';
    }
}
MyString::MyString( const MyString& str)
{
    m_buf = new char[ strlen(str.m_buf) +1 ];
    strcpy( m_buf, str.m_buf);
}

MyString::~MyString( )
{
    delete[] m_buf;
}
void MyString::ShowString( )
{
    if(*m_buf!= '\0')
    {
        cout<< m_buf <<endl;
    }
    else
    {
        cout << "This is a NULL string" << endl;
    }
}

void MyString::Add(const MyString& str)
{
    if( *str.m_buf == '\0' )
        return;
    int nLength = GetLength();
    char *pTmp = new char[nLength];
    strcpy(pTmp, m_buf );
    delete [] m_buf;
    m_buf = new char[ (nLength-1) + str.GetLength()  ];
    strcpy(m_buf, pTmp);
    strcat( m_buf, str.m_buf);
    delete[] pTmp;
}
void MyString::Copy(const MyString& str)
{
    delete [] m_buf;
    m_buf = new char[ strlen(str.m_buf) +1 ];
    strcpy( m_buf, str.m_buf);
}
int MyString::GetLength() const
{
    int i = (int)strlen(m_buf)+1;
    return i;
}

int main()
{

    MyString str("hi ");
    MyString str2("You are welcome!");
    str.ShowString();
    str2.ShowString();
    str.Add(str2);
    str.ShowString();
    return 0;
}

2.5 C++能自动产生成员函数

C++能自动产生的成员函数(空类)
构造函数 类名{}
拷贝构造函数 类名(类名& ){}
赋值函数 类名& operator =(类名& );
析构函数 ~类名(){}
空类在不同的编译器下占几个字节。
class Empty{};
const Empty e1; // 缺省构造函数
Empty e2(e1); // 拷贝构造函数
e2 = e1; // 赋值运算符重载函数
最后调用析构函数。
生成的析构函数一般是非虚的。除非是从虚析构函数的基类继承而来


2.6 友元

友元机制:允许一个类将对其非公有成员的访问权授予指定的函数或类。
友元的声明以关键字 friend开始。它只能出现在类定义的内部
注意:

  1. 友元声明可以出现在类中的任何地方,但通常将友元声明成组地放在类定义的开始或结尾。
  2. 友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响。

友元分类
方法一:声明整个类为友元
friend class RepairMan;

方法二:声明类中的某个成员函数为友元
friend bool RepairMan::RepairClock(Clock& clock);

方法三:声明一个非成员函数为友元。(其他类的非成员函数,自身)
friend bool RepairClock(Clock& clock);

特性
友元函数不是类的成员,因此不受类的作用域管辖。
可以访问对象的私有成员,但必须由对象名来引导。(对象为媒介,通过友元机制)

性质
友元关系是单向的。
B类是A类的友元,则B类的函数可以访问A类的私有和保护数据,但A类的成员函数却不能访问B类的私有、保护数据。(孙悟空钻进铁扇公主肚里。)
友元关系不能传递 朋友的朋友不见得是朋友!
友元关系不能继承
如果B类是A类的友元, B类的子类不会自动成为A类的友元类。(借来的东西不是遗产。)


2.7 类成员的补充

const 数据成员
const 成员函数
static 数据成员
static 成员函数

const数据成员 (对象诞生之后其const数据成员不能改变)

const string m_serial_number;
const成员函数
const修饰this指针
this不再是:Type* const this;而是:const Type* const this;
const需要在声明和定义中写出
const成员函数内部无法修改数据成员
const函数内部无法调用non-const成员函数
const 对象 不可以访问 非const 成员函数

例子解析:
1、如果没有const成员函数,使用const对象调用成员函数会出错
不能将“this”指针从“const Clock”转换为“Clock &”转换丢失限定符
2、常对象只能调用常成员函数;在常函数和非常函数并存时,非常对象只能调用非常函数,在只有常函数时,非常对象可以调用常函数。

static数据成员

non-static 数据成员是对象属性的成员。
static 数据成员是类属性的成员。

内存:堆、栈、data、code
数据成员变量: int a 堆
主函数main()、静态函数、成员函数code
静态数据成员 data
Int main(int argc,char **avgr){} 参数放在栈中
Class A{};
A *p; *p 栈

类属性
静态数据成员
用关键字static声明;
该类的所有对象维护着同一份拷贝;
不能在构造函数中而应在类外单独进行初始化。必须在程序运行之前完成,并且用(::) 来指明所从属的类名。
声明: //.h class 内
static 类型 成员名;
定义: //.cpp 内(无static)
类型 类名::静态成员名 = 初值; or
类型 类名::静态成员名(参数表);

静态成员函数
类外的代码可以使用类名和作用域操作符(::)来调用静态成员函数;
静态成员函数只能访问属于该类的静态数据成员或静态成员函数,不能使用非静态成员。
没有this指针,所以不能为常函数。
函数声明有static修饰,如果类外实现,不加static修饰

使用:
a) 对象名.XXX //指针->XXX
b) 类名::XXX //建议采用这种方式


2.8 简单内存对象模型

组合
class A{}; class B{A a;};
初始化:
类名::类名(形参总表) :对象1(参数),对象2(参数)
{
构造函数的其他功能
}
给类作对象布局的安排:让类的对象中含有一个别的类对象,在本对象的适当位置处,调用那个类的构造函数,来产生内嵌对象作为本类的数据成员。
聚合(指针或引用,关系性紧密)
Class A{}; class B{A *a;};
例子:
class Point
{
private:
float x, y; //点的坐标
public:
Point(float h, float v); //构造函数

float GetX(void){return x;} //取X坐标取坐标
float GetY(void){return y;} //Yvoid Draw(void); //在(x,y)处画点

};

Point::Point(float h, float v){//构造函数
x = h;
y = v;
}

class Line
{
private:
Point p1,p2; //线段的两个端点
public:
Line(Point a,Point b):p1(a), p2(b)
{ //构造函数

}
void Draw(void); //画出线段

};
void main()
{
Point point1(1,1);
Point point2(1,2);
Line line1(point1, point2);
}

上一篇下一篇

猜你喜欢

热点阅读