类与对象基础总结

By AverageJoeWang
 标签:

零.概述与问题

概述

  • 在C语言中,程序是由一个个函数组成的,是结构化的面向过程的编程方法。

  • C++语言进行面向对象的程序设计,编写的程序是由对象组成的。

  • 面向对象的三(四)大特性:封装、继承、多态、(抽象)

问题

  • 复制构造函数为什么一定为引用?

  • struct和class有什么区别?

一.类

  • C++用类来描述对象,类是对现实世界中相似事物的抽象。

  • 类的定义分为两个部分:成员变量(数据、属性)和成员函数(对数据的操作、行为)。

  • 类是对象的封装,对象是类的实例。

  • 成员变量占据不同的内存区域(堆、栈);成员函数共用同一内存区域(代码段)。

1.1 类定义

class 类名
{    
 //class和struct的唯一区别在于:struct的默认访问方式是public,而class为private。 
private:
  // 私有成员变量和函数:只能由本类的函数访问,一般将数据成员写在这里
protected:
   //保护成员变量和函数:只能在派生类中访问,和继承相关
public:
   //公共成员变量和函数:在派生类和类外均可访问,一般将成员函数写在public
}; //不要漏写了这个分号;

1.2 类的实现

就是定义其成员函数的过程,类的实现有两种方式:

  • 1)在类定义时同时完成成员函数的定义。

  • 2)在类定义的外部定义其成员函数。

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

二.构造函数

  • 函数的名字与类名相同;

  • 没有返回类型和返回值(void也不能有)。

其主要工作有:

  • 1)给对象一个标识符。

  • 2)为对象数据成员开辟内存空间。

  • 3)完成对象数据成员的初始化。

2.1 构造函数的定义

  • 一旦程序员为一个类定义了构造函数,编译器便不会为类自动生成缺省(默认)构造函数,因此,如果还想使用无参的构造函数,就必须在类定义中显式定义一个无参构造函数实现原理是构造函数支持重载。

  • 构造函数允许按参数缺省方式调用:构造函数使用默认参数

  • 构造函数分为默认构造函数有参数构造函数复制构造函数

2.2 初始化数据成员

  • 1)在构造函数体内初始化数据成员;

  • 2)通过成员初始化表达式来完成。

point(int x,int y){    //构造函数
cout << "有参构造函数的调用" << endl;
xPos=x;
yPos=y;
}
//等价于:列表初始化
point(int x,int y):xPos(x),yPos(y){    //成员初始化表达式
cout<<"有参构造函数的调用"<<endl;
}

注意:当成员中有指针变量时,在显式定义无参构造函数时,若直接赋值NULL,在成员函数的cout语言中程序会崩溃,且无提示。应使用以下的定义方式:

Computer():_price(0){
    _brand=new char[1];
}

2.3 初始化的顺序

  • 1)和声明时的顺序一致;

  • 2)初始化成员列表的赋值语句先执行,构造函数体中的赋值语句后执行。

三.析构函数

  • 1)与类同名,之前冠以波浪号(~),以区别于构造函数。

  • 2)析构函数没有返回类型,也不能指定参数,因此,析构函数只能有一个,不能被重载

  • 3)对象超出其作用域被销毁时,析构函数会被自动调用。

3.1 定义

  • 注意:缺省析构函数是个空的函数体,只清除类的数据成员所占据的空间,但对类的成员变量通过new和malloc动态申请的内存无能为力,因此,对于动态申请的内存,应在显式定义的析构函数中通delete或free进行释放,避免对象撤销造成的内存泄漏。

显式定义析构函数:

~computer(){ 
    if(_brand != NULL){ 
        delete [] _brand; //对象撤销时,释放内存,避免泄露。_brand = new char[1];
        _brand = NULL;//防止在显式调用后,对象释放时析构函数又被隐式调用,重复释放会报错。
    }
}

3.2 构造函数和析构函数的调用

  • 构造函数在创建对象时被系统自动调用,而析构函数在对象被撤销时被自动调用。

针对不同类型的对象,调用的时间不同:

  • 1)对于全局定义的对象:每当程序开始运行,在主函数main(或WinMain)接受程序控制权之前,就调用全局对象的构造函数。整个程序结束时调用析构函数。

  • 2)对于局部定义的对象:每当程序流程到达该对象的定义处就调用构造函数,在程序离开局部变量的作用域时调用对象的析构函数。

  • 3)对于关键字static定义的静态局部变量:当程序流程第一次到达该对象定义处调用构造函数,在整个程序结束时调用析构函数。

  • 4)对于用new运算符创建的对象:每当创建该对象时调用构造函数,当用delete删除该对象时,调用析构函数。

四.复制构造函数

4.1 定义

  • 如果类定义中没有显式定义该复制构造函数时,编译器会隐式定义一个缺省的复制构造函数,它是一个inline、public的成员函数。

原型、显式定义的构造函数:

//复制构造函数的定义
//类名::类名(const 类名 &)
point::point (const point &);
//例子:
point(int x=0, int y=0){    //构造函数
    xPos=x;
    yPos=y;
}
point(const point & pt){    //复制构造函数
//1.若去掉&直接传递对象,会造成无限递归;
//2.传引用时,为了能够绑定到右值(临时对象)xPos、yPos,加const。
    xPos=pt.xPos;
    yPos=pt.yPos;
}

4.2 浅拷贝和深拷贝

  • 缺省的构造函数会带来问题:两个对象的指针都指向同一块内存,两个析构函数delete[]同一块内存出错(即浅拷贝)。当属性中有指针,且存放堆空间的数据,不要进行浅拷贝的操作,要进行深拷贝

深拷贝:显式定义复制构造函数,并为其开辟动态内存

computer(const computer &cp) //自定义复制构造函数
{
//重新为_brand开辟和cp._brand同等大小的动态内存
_brand = new char[strlen(cp._brand) + 1];
strcpy(_brand, cp._brand); //字符串复制
_price = cp._price;
}

4.3 复制构造函数的自动调用

  • 1)当把一个已经存在的对象赋值给另一个新的对象时。
//Point 类
Point pt1(3,4);    //默认构造函数
Point pt2(pt1);    //复制构造函数1
Point pt3=pt1;    //复制构造函数2
  • 2)当实参和形参都是对象,进行形参和实参的结合时。
void display(Point pt){    //Point pt=p1; 是形参和实参的结合之时,本质上和1)中的情况相同。
    pt.print();
}
int main(){
    Point p1(3,4);    //默认构造函数
    display(p1);    //复制构造函数
}
  • 3)当函数的返回值是对象,函数调用完成返回时。
Point getPoint(){
    Point p(1,2);
    return p;
}
int main(){
    getPoint();    //默认构造函数+
                  //关闭编译器的返回值优化(参数:-fno-elide-constructors)才能看见调用了复制构造函数
}

4.4 隐式转换

Point(int ix=0, int iy=0);            //取消隐式转换的方法:
Point(int ix, int iy);                //1.取消默认参数
explicit Point(int ix=0, int iy=0);   
//2.**添加explicit参数**:表示构造函数必须显式调用
Point p4=5;    
/*
隐式转换:
1.先调用与参数=5相匹配的构造函数(若有);
2.创建了一个临时Point对象,再调用复制构造函数。
*/
  • 取消隐式转换后,不能通过int 5来创建一个Point类的对象。

五.赋值运算符重载函数

  • 如果不重载赋值运算符,编译器会自动为每个类生成一个缺省的赋值运算符重载函数,先看下面的语句:
对象1=对象2;

该语句实际上是完成了由对象2各个成员到对象1相应成员的复制,其中包括指针成员,这和复制构造函数类似,如果对象1中含指针成员,并且牵扯到类内指针成员动态申请内存时,问题就会出现。这时候需要显式定义赋值运算符重载函数。

5.1 定义

Computer & operator=(const Computer & rhs){
    //1.自复制
    if(this==&rhs)
        return *this;
    //2.释放对象之前的所开空间
    delete [] _brand;
    //3.创建新的空间,执行新的复制操作
     _brand=new char[strlen(rhs._brand)+1];
    strcpy(_brand,rhs._brand);
    price=rhs._price;
    return *this;
}

5.2 与复制构造函数的区别

  • 1)复制构造函数是用已经存在的对象的各成员的当前值来创建一个相同的新对象。
类名 对象1=对象2;    //复制构造函数
  • 2)赋值运算符重载函数要把一个已经存在对象的各成员当前值赋值给另一个已经存在的同类对象。
类名 对象1;    //默认构造函数
对象1=对象2;    //赋值运算符函数
  • 例子
Point pt1;//调用默认构造函数
Point pt2 = pt1;//调用复制构造函数
Point pt3;//调用默认构造函数
pt3 = pt2;//调用赋值运算符函数

六.特殊数据成员的初始化

  • 有4类特殊的数据成员:常量成员、引用成员、类对象成员、静态成员。它们的初始化和使用方式与普通数据成员有所不同。

6.1 const数据成员的初始化

  • 只能通过成员初始化表进行初始化。
#include<iostream>
using std::cout;
using std::endl;
class Point
{
public:
    Point(int ix, int iy);//默认构造函数
    Point(const Point & rhs);//复制构造函数
    void print();
private:
    const int _ix;//常量的初始化必须放在初始化列表里面进行
    const int _iy;
};

Point::Point(int ix, int iy)
: _ix(ix)
, _iy(iy)//正确
{
    //_ix = ix;
    //_iy = iy;//错误,常量的初始化必须放在初始化列表里面
    cout << "Point(int , int)" << endl;
}

Point::Point(const Point & rhs)
: _ix(rhs.ix)
, _iy(rhs.iy)
{
    cout << "Point(const Point &)" << endl;
}

void Point::print()
{
    cout << "(" << _ix << ", " << _iy << ")" << endl;
}

int main()
{
    Point pt1(3, 4);
    pt1.print();
    return 0;
}
Point (int x, int y):_x(x),_y(x){
//_x=x;  //error1:uninitialized member "_x" with const type; 
//_y=y;  //error2:assignment of read-only member;
}

6.2 引用成员的初始化

  • 引用成员的初始化要放在初始化表中。
Point(int x, int y, double & z)
: _ref1(_x)
,  _ref2(z){
    _x=x;
    _y=y;
}

6.3 类对象成员的初始化

  • 类数据成员也可以是另一个类的对象,比如,一个直线类对象中包含两个point类对象,在直线类对象创建时可以在初始化列表中初始化两个point对象。
private:
    Point pt1;
    Point pt2;

6.4 static数据成员的初始化

  • 必须在类申明之外进行,且不再包含static关键字。
类型 类名::变量名=初始化表达式;
类型 类名::对象名(构造参数);

Example:

class Computer{
    float _price;
    static float _total;
public:
    Computer(const float p){
        _price=p;
        _total+=p;
    }
    ~Computer(){
        _total-=_price
    }
};
float Computer::_total=0;    //initialized

七.特殊成员函数

7.1 静态成员函数

  • 静态成员函数体内不能使用非静态的成员变量和非静态的成员函数;只能调用静态成员数据和函数。

  • 静态成员函数的参数列表中不含有this指针。故不能访问非静态数据成员,但是可以传参。

Example:

  • 1)静态数据成员的初始化(必须在类定义之外);

  • 2)静态成员函数的原型声明(访问非静态数据成员需要传参);

  • 3)静态成员函数的实现(类定义的内外均可);

  • 4)直接通过“类名::”进行调用,无需创建类对象。

#include <iostream>
#include <cstring>
using namespace std;
class Computer{
private:
    char *_name;
    float _price;
    static float _total;
public:
    Computer(const char* chr,const float p){
        _name=new char[strlen(chr)+1];
        strcpy(_name,chr);
        _price=p;
        _total+=p;
    }

    ~Computer(){
        if(_name){
            delete[] _name;
            _name=NULL;
        }
        _total-=_price;
    }
    static void total(){    //静态成员函数,原则上只能访问静态数据成员
        cout<<"总价:"<<_total<<endl;
    }
    /*静态成员函数print()原型,如果要访问非静态数据成员,必须传递参数。*/
    //static void print(Computer & com);
    static void print(Computer & com){    //静态成员函数print()实现(类定义之内)
        cout<<"名称"<<com._name<<endl;
        cout<<"价格"<<com._price<<endl;
    }
};
//void Computer::print(Computer & com){    //静态成员函数print()实现(类定义之外)
//    cout<<"名称"<<com._name<<endl;
//    cout<<"价格"<<com._price<<endl;
//}
float Computer::_total=0;    //静态数据成员初始化
int main(){
    cout<<"买入电脑1:"<<endl;
    Computer comp1("IBM",7000);
    Computer::print(comp1);//类名加作用域限定符访问static成员函数
    Computer::total();
    cout<<"买入电脑2:"<<endl;
    Computer comp2("ASUS",5000);
    Computer::print(comp2);
    Computer::total();
    cout<<"退回电脑2:"<<endl;
    comp2.~Computer();
    Computer::total();
    return 0;
}

7.2 const成员函数

  • 在const成员函数中,不能修改对象的数据成员,不能调用非const成员函数。因为修改了this指针:
void print() const; 
void print(const Point * const this) const;

基本定义格式

//1.类内定义时:   
类型 函数名(参数列表)const{   
    函数体;   
}   
//2.类外定义时,分2步:   
类内声明:   
类型 函数名(参数列表)const;   
类外定义:   
类型 类名::函数名(参数列表)const{   
    函数体;   
}

Example:

#include <iostream>
using namespace std;
class Point{
    int _x;
    int _y;
public:
    Point(int x=0, int y=0){
        _x=x;
        _y=y;
    }
    void print() const{
        //_x=5;    //试图修改x将引发编译器报错;error:assignment of member '_x' in read-only object
        set();    //试图调用非const函数将报错;error:passing 'const Point'as 'this' argument of 'set()' discards qualifiers
        cout<<"x:"<<_x<<endl;
        cout<<"y:"<<_y<<endl;
    }
    //void set(){
    //}
    void set()const{    //非const对象可以调用const成员函数
    }
};

int main(){
    Point pt;
    pt.print();
    return 0;
}

八.对象

8.1 const对象

  • 非const对象,提供了const成员函数,但是没有提供非const版本,非const对象是可以调用const成员函数的。

  • const对象,它只能调用const成员函数,const对象不能修改。

  • const对象,可以调用非const的构造和析构函数,且可以在函数体中修改对象。

Example:

#include <iostream>
using namespace std;
class Point{
    int _x;
    int _y;
public:

    Point(int x=0, int y=0){
        _x=x;
        _y=y;
    }

    ~Point(){
        _x=-1;    //修改了对象
    }

    void SetX(int x){
        _x=x;
    }

    void SetY(int y){
        _y=y;
    }

    void print() const{
        cout<<"_x:"<<_x<<endl;
        cout<<"_y:"<<_y<<endl;
    }
};

int main(){
    Point pt(3,4);
    pt.SetX(5);
    pt.SetY(6);
    pt.print();
    const Point ptc(1,2);
    //ptc.SetX(8);    //错误,const对象只能调用const成员函数
    //ptc.SetY(9);    
    ptc.~Point();    //const对象可以调用非const类型的析构函数
    ptc.print();
    return 0;
}

8.2.对象的大小

  • 对象在内存中是以结构形式(不包括static数据成员)存储在数据段或堆中

  • 注意边界对齐

#include <string.h>
#include <iostream>
using namespace std;
class date{
int a;// 4
double b;//4(waste) + 8
float c;//4
short d[6];//4 + 8
char e[5];//5
char & f;//3(waste) + 8
double* g;// 8
};
int main(){
cout << "sizeof(date) = " << sizeof(date) << endl;//56
return 0;
}

8.3.this指针

8.3.1.关于this指针的几点说明:

  • 1)在每一个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称为this。它是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。

  • 2)this指针是隐式使用的,它是作为参数被传递给成员函数的。

  • 3)编程序者不必人为地在形参中增加this指针,也不必将对象a的地址传给this指针,这些功能由编译系统自动实现.

8.3.2.this指针只能在一个类的成员函数中调用,它表示当前对象的地址。

下面是一个例子:

    void Date::setMonth( int mn )
    {
     month = mn; // 这三句是等价的
     this->month = mn;
     (*this).month = mn;
    }

8.3.3.this只能在成员函数中使用。全局函数,静态函数都不能使用this。

实际上,成员函数默认第一个参数为T* const register this。
如:

class A{public: int func(int p){}};
  • 其中,func的原型在编译器看来应该是:
int func(A* const register this, int p);

8.3.4.this在成员函数的开始前构造的,在成员的结束后清除。

  • 这个生命周期同任一个函数的参数是一样的,没有任何区别。

  • 当调用一个类的成员函数时,编译器将类的指针作为函数的this参数传递进去。

如:

A a;
a.func(10);

此处,编译器将会编译成:

A::func(&a, 10);
  • 看起来和静态函数没差别,不过,区别还是有的。

  • 编译器通常会对this指针做一些优化的,因此,this指针的传递效率比较高--如vc通常是通过ecx寄存器来传递this参数。

8.3.5.相关问题及其回答:

  • 1)this指针是什么时候创建的?

this在成员函数的开始执行前构造的,在成员的执行结束后清除。

  • 2)this指针存放在何处? 堆,栈,全局变量,还是其他?

this指针会因编译器不同,而放置的位置不同。可能是栈,也可能是寄存器,甚至全局变量。

  • 3)this指针如何传递给类中函数的?绑定?还是在函数参数的首参数就是this指针.那么this指针又是如何找到类实例后函数的?

this是通过函数参数的首参数来传递的。this指针是在调用之前生成的。类实例后的函数,没有这个说法。类在实例化时,只分配类中的变量空间,并没有为函数分配空间。自从类的函数定义完成后,它就在那儿,不会跑的。

  • 4)this指针如何访问类中变量的?

如果不是类,而是结构的话,那么,如何通过结构指针来访问结构中的变量呢?如果你明白这一点的话,那就很好理解这个问题了。

在C++中,类和结构是只有一个区别的:类的成员默认是private,而结构是public。

this是类的指针,如果换成结构,那this就是结构的指针了。

  • 5)我们只有获得一个对象后,才能通过对象使用this指针,如果我们知道一个对象this指针的位置可以直接使用吗?

this指针只有在成员函数中才有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们也无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以&this获得),也可以直接使用的。

  • 6)每个类编译后,是否创建一个类中函数表保存函数指针,以便用来调用函数?

普通的类函数(不论是成员函数,还是静态函数),都不会创建一个函数表来保存函数指针的。只有虚函数才会被放到函数表中。但是,既使是虚函数,如果编译器能明确知道调用的是哪个函数,编译器就不会通过函数表中的指针来间接调用,而是会直接调用该函数。

8.3.6.问题的提出:编写程序实现对象资源的拷贝(要求使用this指针)。


#include<iostream>
using namespace std;

class student{
private:
 char *name;
    int id;
public:
    student(char *pName="no name",int ssId=0)
    {
           id=ssId;
     name=new char[strlen(pName)+1];
     strcpy(name,pName);
     cout << "construct new student" << endl;
    }

    void copy(student &s)
    {
           if (this == &s)
        {
            cout<<"Erro:can't copy one to oneself!"<<endl;
            return;
           }else
         {
            name=new char[strlen(s.name)+1];
            strcpy(name,s.name);
            id=s.id;
            cout<<"the function is deposed!"<<endl;
        }
    }

 void disp()
    {
     cout<<"Name:"<  Id:"<<endl;
    }
 ~student()
    {
     cout<<"Destruct "<<endl;
     delete [] name;
    }
};

int main()
{
  student a("Joe",12),b("Tom",23);
  a.disp();
  b.disp();
  a.copy(a);
  b.copy(a);
  a.disp();
  b.disp();
return 0;
}