C++类和对象

一、C++类和对象

1.c++类的定义

什么是类?什么是对象?类和对象的理解是整个语言学习中核心的基础。通俗的理解,类其实就是一个模子,是一个变量类型,对象就是这个类型定义出来的具体的变量,就像int a;这句话,int对应类,a就对应对象。这样大家应该就好理解了,但需要注意的是int是C++的内置类型,并不是真正的类。
类是对象的抽象和概括,而对象是类的具体和实例。

c ++中类的其实就是包含函数的结构体!因为C++类里面的成员除了可以像C语言的结构体那样包含基本变量以外,还可以包含函数,前者叫做成员变量,后者叫做成员方法。

关键字用class类定义,比如下面定义一个C++的类,学生类:

class Student
{
public:
    int num;
    char name[100];
    int score;
    int print()
    {
        cout<<num<<" "<<name<<" "<<score;
        return 0;
    }
};

类里还有一个public的东西,它是控制成员访问权限的一个存取控制属性,除了public以外,还有private、protected一共三种。其中private表示私有,被它声明的成员,仅仅能被该类里的成员访问,外界不能访问,是最封闭的一种权限;protected比private稍微公开一些,除了类内自己的成员可以访问外,它的子类也可以访问(关于子类的概念我们会在后面详细展开);而public声明的成员,则可以被该类的任何对象访问,是完全公开的数据。

刚才看的这种写法,成员函数是写在类里的,如果类里的成员函数很多的话,阅读起来就会乱很多,故此,C++还支持另外一种写法,就是成员函数仅在类内声明函数原型,在类外定义函数体,这样在类里可以看到所有成员函数的列表,像目录一样一目了然,规范很多。

在类中声明函数原型的方法与一般C语言的函数原型声明一样,而在类外定义函数的方法,则需要类名加::作用域限定符表示,我们还以刚才的Student类为例,类外定义的代码如下:

class Student
{
public:
    int num;//学号
    char name[100];//名字
    int score;//成绩
    int print();//类内声明print函数
};
int Student::print()//在类外定义完整的print函数
{
    cout<<num<<" "<<name<<" "<<score;
    return 0;
}

注意:print函数在类里声明后,我们在后面又完整的定义了出来,函数头部分在返回值和函数名之间用类名加::的方式指明这个函数是属于哪个类的。

2.C++对象的建立和使用

类是对象的抽象和概括,而对象是类的具体和实例

  1. 对象的创建:
    类就是包含函数的结构体,是一种自定义数据类型,用它定义出来变量,就是对象,这就是所谓的“对象是类的具体和实例”,定义了一个这个类的对象,也可以说实例化了一个对象,就是这个意思!
    而对象的使用,和结构体的使用也一样,都是主要访问里面的成员,也都是用过.的方式来访问,如:
Student A;
A.num = 101;
strcpy(A.name,"dotcpp");
A.score = 100;
A.print();

需要注意的是,这里类中的成员变量都是声明为public类型的,如果声明为private类型,则在主函数中主要通过对象.变量的方式直接访问的话就会被禁止报错,原因private类型的变量是私有类型,不允许外部访问。

对于想保护但又想控制的私有变量,我们通常将其声明为private类型,然后同时定义一个public类型的专门赋值的方法,由于内部成员可以访问private声明的变量,我们就可以在外部通过这个public的方法来间接控制这些私有的成员,来起到封装、保护的效果,而这个public类型的方法,也称之为这个类的一个外部接口。

  1. 对象的指针:
    与普通变量一样,对象也是一片连续的内存空间,因此也可以创建一个指向对象的指针,即对象指针,存储这个对象的地址。

那么创建方法与使用一般类型的指针类似,定义方法如下:

类名 *指针名;

如定义Student *p;定义一个Clock类型的指针p,需要清楚的是,这里并没有建立对象,当然也不会调用构造函数。接下来就可以将一个同类型的类对象地址赋值给这个指针,然后通过->来访问对象中的成员。
代码如下:

Student *p;
Student A;
p = &A;
p->print();

以上是对象指针的使用方法,除了在赋值、访问成员的时候用以外,在传参的时候也建议用指针来传递,因为其传递的为地址,不会进行对象之间的副本赋值,从而减少内存的开销,提高效率。

  1. 对象的引用:
    引用,**是C++中一种新的类型,对象引用就是一个类对象起个别名,本质上也是把这个类对象的地址赋给了这个引用类型,两者是指向一块内存空间的。**那么如何定义使用?下面给大家展示。
Student A;
Student &Aq=A;

如以上代码,定义一个Student类型的对象,然后用&来定义一个该类类型的引用类型,并把A对象赋给Aq作为初始化。
需要注意的是:

  1. 与指针一样,两者必须是同类型才可以引用。
  2. 除非做函数的返回值或形参时,其余定义引用类型的同时就要初始化!
  3. 引用类型并非是新建立一个对象,因此也不会调用构造函数。

那么既然是类对象的别名,因此使用方法也和类对象一样,用别名.成员的方法进行访问,如:

Student A;
Student &Aq=A;
Aq.print();

用引用类型时,本质还是存的地址,因此无论传参定义都不会太多内存开销,有指针的优势,同时使用起来和类对象本身使用一样,再做函数实参时,直接传入引用对象就可以,不用加地址符,因此看起来更直观、方便。这就是引用类型的优点。

3.C++中的构造函数(Constructor)

C++中有这么一种特殊的函数,它在类里,与类名同名,且没有返回值的一个函数,只要我们定义一个类的对象,系统就会自动调用它,进行专门的初始化对象用,而大多数情况下,因为我们没有定义构造函数,系统会默认生成一个默认形式、隐藏着的构造函数,这个构造函数的函数体是空着的,因此不具有任何功能。
如何定义自己的构造函数,需要用户自行定义了至少一个构造函数,系统就不在自动生成,而是根据用户定义的构造函数选择最匹配的一个进行调用。

例如还是Student类的例子,我们添加一个带有默认参数的构造函数,代码如下:

#include<iostream>
#include<Cstring>
using namespace std;
class Student
{
    private:
    int num;//学号
    char name[100];//名字
    int score;//成绩
    public:
    Student(int n,char *str,int s);
    int print();
    int Set(int n,char *str,int s);
};
Student::Student(int n,char *str,int s)
{
     num = n;
     strcpy(name,str);
     score = s;
cout<<"Constructor"<<endl;
}
int Student::print()
{
    cout<<num<<" "<<name<<" "<<score;
    return 0;
}
int Student::Set(int n,char *str,int s)
{
     num = n;
     strcpy(name,str);
     score = s;
}
int main()
{
    Student A(100,"dotcpp",11);
    A.print();
    return 0;
}

需要注意的是,由于我们已经在类中定义了一个带默认参数的构造函数,则系统不会再自动生成,这个时候我们定义对象的时候也需要传入三个默认初始值,因为构造函数可以重载,系统会找最匹配的一个函数,但如果定义时不是带默认参数的构造函数,则会报错

4.C++中的析构函数(Destructor)

除了上一节讲到的类对象在创建时自动调用的构造函数,在对象销毁时也会自动调用一个函数,它也和类名同名**,也没有返回值,名字前有一个波浪线~,**用来区分构造函数,它的作用主要是用做对象释放后的清理善后工作。它就是析构函数。
与构造函数相同的是,与类名相同,没有返回值,如果用户不定义,系统也会自动生成一个空的析构函数。而一旦用户定义,则对象在销毁时自动调用。

我们以Student类为例,继续添加析构函数,同时在构造函数和析构函数中都添加了输出当前类的信息,用来辨别哪一个类的创建和销毁,请大家仔细阅读代码:

#include<iostream>
#include<Cstring>
using namespace std;
class Student
{
    private:
    int num;//学号
    char name[100];//名字
    int score;//成绩
    public:
    Student(int n,char *str,int s);
    ~Student();
    int print();
    int Set(int n,char *str,int s);
};
Student::Student(int n,char *str,int s)
{
     num = n;
     strcpy(name,str);
     score = s;
     cout<<num<<" "<<name<<" "<<score<<" ";
     cout<<"Constructor"<<endl;
}
Student::~Student()
{
    cout<<num<<" "<<name<<" "<<score<<" ";
    cout<<"destructor"<<endl;
}
int Student::print()
{
    cout<<num<<" "<<name<<" "<<score<<endl;
    return 0;
}
int Student::Set(int n,char *str,int s)
{
     num = n;
     strcpy(name,str);
     score = s;
}
int main()
{
    Student A(100,"dot",11);
    Student B(101,"cpp",12);
    return 0;
}

在这里插入图片描述

看到对象A和B的构造函数的调用顺序以及构造函数的调用顺序,完全相反!原因在于A和B对象同属局部对象,也在栈区存储,也遵循“先进后出”的顺序!

5.C++拷贝构造函数实例详解

拷贝构造函数

在C++中,与类名同名,且形参是本类对象的引用类型的函数,叫做拷贝构造函数(Copy Constrctor),与构造函数一样,当我们不主动定义的时候,系统也会自动生成一个,进行两个对象成员之间对应的简单赋值,用来初始化一个对象,如以下的情形:

#include<iostream>
using namespace std;
#define PI 3.1415
class Circle
{
    private:
    double R;
    public:
    Circle(double R);
    Circle(Circle &A);
    double area();
    double girth();
};
Circle::Circle(double R)
{
    cout<<"Constructor"<<endl;
    this->R = R;
}
Circle::Circle(Circle &A)
{
    cout<<"Copy Constructor"<<endl;
    this->R = A.R;
}
double Circle::area()
{
    return PI*R*R;
}
double Circle::girth()
{
    return 2*PI*R;
}
int main()
{
  
    Circle A(5);
    Circle B(A);
    return 0;
}

定义了一个Circle圆形类,分别定义了带参数的构造函数和拷贝构造函数,然后在主函数中定义A对象,并传入初始值,调用带参数的构造函数。及定义B对象,通过A对象来初始化B对象。运行结果如下:
在这里插入图片描述
第一次定义的A对象调用带参数的构造函数,第二个B对象由于是通过A对象来初始化,所以调用拷贝构造函数。

所以,当类中有指针类型时,依靠默认的拷贝构造函数的方法,已经无法满足我们的需求,必须定义一个特定的拷贝构造函数,即不仅可以进行数据的拷贝,也可以为成员分配内存空间,实现真正的拷贝,也叫做深拷贝,这就是深拷贝构造函数。

6.C++浅拷贝与深拷贝实例详解

浅拷贝与深拷贝

在上一节讲解的拷贝构造函数的例子Circle类中,拷贝的策略都是与系统默认的策略一致,即把原有对象中成员依次拷贝给新对象中对应的成员,既然如此,我们为何还要自己定义呢?原因在于,简单的将所有情况都按照这种简单的方式初始化,难免有不同的情况,出现问题。
例如,刚才的Circle类中,如果成员变量中加一个指针成员,初始化中需要动态开辟内存,则会出现极大的安全隐患,代码如下:

#include<iostream>
#include<Cstring>
using namespace std;
#define PI 3.1415
class Circle
{
    private:
    double R;
    char *str;
    public:
    Circle(double R,char *str);
    ~Circle();
    double area();
    double girth();
};
Circle::~Circle()
{
    delete []str;
}
Circle::Circle(double R,char *str)
{
    cout<<"Constructor"<<endl;
    this->R = R;
    this->str = new char[strlen(str)+1];
    strcpy(this->str,str);
    cout<<this->R<<" "<<this->str<<endl;
}
  
double Circle::area()
{
    return PI*R*R;
}
double Circle::girth()
{
  
    return 2*PI*R;
}
int main()
{
  
    Circle A(5,"NO.1 Old class");
    Circle B(A);
    return 0;
}

为了验证,在Circle类中,我们增加了一个指针成员,并且在构造函数中对其初始化,同时没有自定义拷贝构造函数。那么在主函数中Circle B(A);的这句话将A对象赋值给B对象,将调用默认生成的拷贝构造函数,运行后,程序如下图报错:
在这里插入图片描述

而实际上的原因在于,默认的拷贝构造函数仅仅是进行数据赋值,并不能为指针开辟内存空间,相当于代码:

This->str = str;

那么本质上,也就是两个指针指向一块堆空间。已经违背了我们的初衷。那么在程序结束的时候,两个对象回收的时候,会调用自己的析构函数,释放这块内存空间,由于两个对象要调用两次,即delete两次,就会出现错误!

#include<iostream>
#include<Cstring>
using namespace std;
#define PI 3.1415
class Circle
{
    private:
    double R;
    char *str;
    public:
    Circle(double R,char *str);
    Circle(Circle &A);
    ~Circle();
    double area();
    double girth();
};
  
Circle::~Circle()
{
    delete []str;
    cout<<"Call Destructor"<<endl;
}
Circle::Circle(Circle &A)
{
    cout<<"Copy Constructor"<<endl;
    this->R = A.R;
    this->str = new char[strlen(A.str)+1];
    strcpy(this->str,A.str);
}
Circle::Circle(double R,char *str)
{
    cout<<"Constructor"<<endl;
    this->R = R;
    this->str = new char[strlen(str)+1];
    strcpy(this->str,str);
}
  
double Circle::area()
{
    return PI*R*R;
}
double Circle::girth()
{
    return 2*PI*R;
}
int main()
{
  
    Circle A(5,"NO.1 Old class");
    Circle B(A);
    return 0;
}

7.C++中的this指针

this指针
一个类当中,有一个很隐蔽的特殊指针,它就是this指针!
为什么说它特殊?**因为只要定义一个类,系统就会预定义个名字叫做this名且指向当前对象的指针。**虽然我们看不到但却可以使用它。

比如,我们来看一个时钟类的一个成员函数,用来设置时间传值的代码:

int Clock::SetTime(int h,int m,int s)
{
   H = h;
   M = m;
   S = s;
}

可以看到Clock类本身的成员变量为H、M、S,要用外部传来的值给它们三个赋值,为了区别它们,我们把形参定义成小写。那么如果我们知道了this的存在,就可以这样写:

int Clock::SetTime(int h,int m,int s)
{
   this->H = h;
   this->M = m;
   this->S = s;
}
//也可以写成:
int Clock::SetTime(int h,int m,int s)
{
   (*this).H = h;
   (*this).M= m;
   (*this).S= s;
}

上两种写法用到了对象中的隐藏的this指针,可以明确是本类中的成员,从而明显的区别本对象与外部变量。实际上,当一个对象调用其成员函数的时候,即便程序中有多个该类的对象,但成员函数的代码也仅有一份,所以为了区分它们是哪个对象调用的成员函数,编译器也是转化成this->成员函数这种形式来使用的。

8.C++友元函数的使用方法

知道类中的私有成员,只有被类里的成员函数访问,在类外是不能访问的。这体现了C++中类设计的封装、隐蔽思想,是C++最基本的优点。
但如果偶尔有的时候,我们在类外又确实想访问这些私有成员,就会变得麻烦很多,就处于既访问不到又不能声明为public类型的两难处境。
友元的出现就可以很好的解决这个问题,即把外部的函数声明为友元类型,赋予它可以访问类内私有成员的权利,来做到两全其美。这就是友元的意义,从字面意思也可以看出来,像“朋友”一样,开了一个绿色通道
友元的对象,它可以是全局的一般函数,也可以是其他类里的成员函数,这种叫做友元函数。不仅如此,友元还可以是一个类,这种叫做友元类。

理解友元的意义和作用后,我们来看怎么在C++中使用。对于友元函数,只需要在类内对这个函数进行声明,并在之前加上friend关键字。这个函数就具有了独特的权限,成为友元函数。

友元并不属于这个类本身,无论是友元函数还是友元类。都不能使用类内的this指针,同时也不可以被继承,如同父亲的朋友不一定是儿子的朋友这个道理。
如:求两点的距离

#include<iostream>
#include<math.h>
using namespace std;
class Point
{
    private:
        double x;
        double y;
    public:
        Point(double a,double b)
        {
            x = a;
            y = b;
        }
        int GetPoint()
        {
            cout<<"("<<x<<","<<y<<")";
            return 0;
        }
        friend double Distance(Point &a,Point &b);
};
//求两点之间的距离
double Distance(Point &a,Point &b)
{
    double xx;
    double yy;
    xx = a.x-b.x;
    yy = a.y-b.y;
  
    return sqrt(xx*xx+yy*yy);
}
int main()
{
    Point A(2.0,3.0);
    Point B(1.0,2.0);
    double dis;
    dis = Distance(A,B);
    cout<<dis<<endl;
    return 0;
}

在这里插入图片描述

9.C++友元类的使用方法

学习友元函数之后,我们再来看友元类,也是一样的道理和使用方法。如果把一个类A声明为另一个类B的友元类,则类A中的所有成员函数都可以访问B类中的成员,使用方法也一样,在类B中进行声明即可。

#include<iostream>
#include<math.h>
using namespace std;
  
class Point
{
    private:
        double x;
        double y;
    public:
        Point(double a,double b)
        {
            x = a;
            y = b;
        }
        int GetPoint()
        {
            cout<<"("<<x<<","<<y<<")";
            return 0;
        }
        int distancetoLine()
        {
  
        }
    friend class Tool;
};
  
class Tool
{
public:
    double GetX(Point &A)
    {
        cout<<A.x<<endl;
        return A.x;
    }
    double GetY(Point &A)
    {
        cout<<A.y<<endl;
        return A.y;
    }
    double dis(Point &A)
    {
        cout<<sqrt(A.x*A.x+A.y*A.y)<<endl;
        return  sqrt(A.x*A.x+A.y*A.y);
    }
};
  
int main()
{
    Point A(2.0,3.0);
    Tool T;
    T.GetX(A);
    T.GetY(A);
    T.dis(A);
    return 0;
}

在这里插入图片描述

可以看到我们又定义了一个工具类,可以获取一个点类的横、纵坐标以及求出这个点距离原点的距离,由三个方法实现,封装到一个类Tool里。并且在Point类里进行友元类的声明,可以看到使用方法。

优点:更方便快捷的访问类内的私有成员。

缺点:打破了C++中的封装思想。

10.C++中常数据的使用及初始化

常数据成员的使用及初始化
常的概念我们在学C语言时候就有了解,关键字是const,所谓的“常”,或者说被“常”修饰的变量,是不可以被改变的,比如用const修饰的一个变量就成了常变量,这个值不可被更改。
C++中,一样有常的概念,额外不同的是,const除了可以修饰一般的变量为常变量之外,还可用于修饰某个对象,变成常对象。以及可以修饰类的数据成员和成员函数,分别叫做类的常数据成员和常成员函数。

  1. 常数据成员:
    对于常数据成员的用法,与我们在C语言的用法一样,只不过这部分数据出现在类里,使用的格式如下:

数据类型 const 数据成员名;
或const 数据类型 数据成员名;

被const修饰的成员则必须进行初始化,并且不能被更改,而初始化的方式则是在类的构造函数的初始化列表里进行的。

另外,有一个特殊情况,如果成员是static类型,即静态常数据成员,因为是静态的属性,初始化则需在类外进行初始化。下面我们把刚才讲的情况放到代码里来说明:

#include<iostream>
using namespace std;
class Clock
{
private:
    const int h;  //修饰h为常类型成员
    const int m;  //修饰m为常类型成员
    int const s;  //和上面两种用法都可以
    static const int x;
public:
    Clock(int a,int b,int c):h(a),m(b),s(c)
    {
        cout<<"Constrctor! Called"<<endl;
    }
    int ShowTime()
    {
        cout<<h<<":"<<m<<":"<<s<<endl;
        return 0;
    }
    int GetX()
    {
        cout<<x<<endl;
        return 0;
    }
};
const int Clock::x = 99;
int main()
{
    Clock A(12,10,30);
    A.ShowTime();
    A.GetX();
    return 0;
}

在这里插入图片描述

注意看类中的四个常数据成员,其中X为static类型,因其静态化的特殊属性则需要在类外初始化,即便X为私有类型!

  1. 常对象:
    C++中可以把一个对象声明为const类型,即常对象。这样声明之后,这个对象在整个生命周期中就不可以再被更改,所以在定义的时候要由构造函数进行初始化,定义格式如下:
    类型 const 对象名;

    const 类型 对象名;

需要注意的是,常对象不可以访问类中的非常成员函数,只能访问常成员函数(后面第三部分)。下面我们看例子:

#include<iostream>
using namespace std;
class Clock
{
private:
    const int h;  //修饰h为常类型成员
    const int m;  //修饰m为常类型成员
    int const s;  //和上面两种用法都可以
    int x;
public:
    Clock(int a,int b,int c):h(a),m(b),s(c)
    {
        x=99;
        cout<<"Constrctor! Called"<<endl;
    }
    int ShowTime()
    {
        cout<<h<<":"<<m<<":"<<s<<endl;
        return 0;
    }
    int GetX() const
    {
        //x=99;
        cout<<x<<endl;
        return 0;
    }
};
  
int main()
{
    const Clock A(12,10,30);
    const Clock B(14,20,50);
    //A = B;
    //A.ShowTime();
    A.GetX();
    return 0;
}

在这里插入图片描述在这里插入图片描述

注意看代码中语句,正常编译将报错,原因是A为常对象不可以被赋值,以及ShowTime函数为非常成员函数。

  1. 常成员函数:

类似的,一个类中的成员函数被const修饰后,就变成了常成员函数,常成员函数的定义如下:

返回类型 函数名(形参表列) const;

需要注意:
(1)常成员函数的定义和声明部分都要包含const;
(2)常成员函数只能调用常成员函数,而不能调用非常成员函数,访问但不可以更改非常成员变量。

#include<iostream>
using namespace std;
class Clock
{
private:
    const int h;  //修饰h为常类型成员
    const int m;  //修饰m为常类型成员
    int const s;  //和上面两种用法都可以
    int x;
public:
    Clock(int a,int b,int c):h(a),m(b),s(c)
    {
        x=99;
        cout<<"Constrctor! Called"<<endl;
    }
    int ShowTime()
    {
        cout<<h<<":"<<m<<":"<<s<<endl;
        return 0;
    }
    int GetX() const
    {
        //x=99;
        cout<<x<<endl;
        return 0;
    }
};
  
int main()
{
    const Clock A(12,10,30);
    A.GetX();
    return 0;
}

在这里插入图片描述

注意第29、30行代码,如果使用29行代码则会编译报错

总结

天才不受意志的支配,直面向美的欣赏,一切美所给予的欢悦,艺术所提供的安慰,是他完全忘却生活的烦恼。天才乐于孤独寂寞,一个人热衷于社交的程度恰正相当于他在理智上贫乏和庸俗的程度。
来自“https://www.dotcpp.com/course”总结

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值