学习目标

  • 掌握类的概念
  • 理解对象与类的关系,掌握对象的创建和使用
  • 掌握构造函数、析构函数的概念及使用方法
  • 掌握内存动态分配的概念和使用方法
  • 掌握对象数组和对象指针。

    一、基本概念

    什么是类

    类是对一组具有共同属性特征和行为特征的对象的抽象,是将相关数据和对这些数据的操作(函数)组合在一起的一种数据类型
    类的属性=数据成员;
    类的行为=操作=方法=成员函数。
    类可以实现数据的封装、隐藏、继承与派生

    类的格式

    ```cpp class 类名 { public: 数据成员或成员函数 protected: 数据成员或成员函数 private: 数据成员或成员函数 };

说明: (1) 一般将类的声明放在头文件.h中,而将成员函数的实现放在.cpp 文件中。 (2) 类定义必须以“;”结束。

  1. <a name="sk1cJ"></a>
  2. ### 结构体与类差异
  3. **与结构体的区别:访问权限、加入函数**
  4. 1. 类声明中的private、protected和public关键字可以按任意顺序出现。为了使程序更加清晰,应将私有成员、保护成员和公有成员归类存放。**默认时的访问权限为私有的(private)**。
  5. 1. 对于一个具体的类,类声明中的private、protected和public三个部分不一定都要有,但至少应该有其中一个部分。
  6. 1. 由于**类是一种数据类型,系统并不会为其分配内存空间**,所以**不能在类声明中给数据成员赋初值**。
  7. ```cpp
  8. [例2-3] 从C语言到C++的实例—学生成绩管理。
  9. struct Student{ 声明一个表示学生的结构体类型
  10. long lNum; //学号
  11. char sName[12]; //姓名
  12. float fGrade; //成绩
  13. };
  14. class OurClass{ //声明一个班级类
  15. private:
  16. char cName[20]; //定义班级名称
  17. Student stu[N]; //定义N个学生
  18. public:
  19. void Input(); //输入学生信息
  20. void Print(); //输出学生信息
  21. void Sort(); //按学生成绩进行排序
  22. };
  23. 声明一个长方形类
  24. 分析:长方形有长和宽,对于长方形可以计算其面积和周长。因此抽象出所有长方形都具有的属性长和宽;然后用成员函数实现求面积和求周长运算。
  25. class Rectangle
  26. {
  27. private:
  28. double length=3.5; 错误,类中不能赋初值
  29. double width=4.6; 错误
  30. public:
  31. double Area();
  32. double Perimeter();
  33. };

成员函数定义

返回类型 类名::函数名(参数表){
//函数体
}

  1. 用类实例化一个对象时,系统会为每一个对象分配存储空间。如果一个类包括了数据成员和成员函数,则要分别为数据和函数的代码分配存储空间。
  2. C++为每个对象的数据成员分配各自独立的存储空间,类似于结构体成员,那么类中的成员函数是否是分配各自独立的存储空间呢?

image.png
公用的存储空间

二、对象的定义和使用

类是一种数据类型,使用时需要定义类的对象!

对象的定义

  • 先声明类。
  • 定义对象 。
  • 类名 对象名列表
    • 例如:Book b1,b2; 此时定义了b1和b2为Book类的两个对象。

      对象的使用

      —————- 对象名.成员函数名(实参表)
      b1.Input(); //通过对象b1执行输入操作
      b1.Print(); //通过对象b1执行输出操作

      类的作用域

      类作用域是指在类的定义中由一对花括号所括起来的部分包括数据成员和成员函数。
      类中的成员函数可以不受限制地访问本类的数据成员和其他成员函数
      作用域外,不能直接访问类的数据成员和成员函数,即使是公有成员,只能通过本类的对象才能访问。

      对象赋值

      同类型的变量,如整型、实型、结构体类型等的变量可以利用赋值操作符(=)进行赋值,对于同类型的对象也同样适用。也就是说,同类型的对象之间可以进行赋值,这种赋值默认通过成员复制进行。当对象进行赋值时,对象的每一个成员逐一复制(赋值)给另一个对象的同一成员。如果类中存在指针,则不能简单的将一个对象的值赋给另一个对象,否则会产生错误

      对象生命周期

  1. 对象的生命周期是指对象从被创建开始到生存期结束为止的时间,对象在定义时被创建,在释放时被终止。
  2. 类中的成员变量的生命周期是在类的对象定义时创建,当对象的生命周期结束后停止。
  3. 局部对象的生命周期是在一个程序块或函数体内;全局对象的生命周期从定义时开始到程序结束时止。 ```cpp [例3-7] 对象生命周期应用举例1。 [例3-8] 对象生命周期应用举例2。(课下阅读) [例3-9] 对象生命周期应用举例3。
<a name="HaqEA"></a>
# 三、构造函数和析构函数

1. **如果变量在使用之前没有正确初始化或清除,将导致程序出错**(例3-6)。因此要求对对象必须正确地进行初始化。对对象进行初始化的一种方法是编写初始化函数,然而很多用户在解决问题时,常常忽视这些函数,以致给程序带来了隐患。为了方便对象的初始化和清理工作,C++提供了两个特殊的成员函数:**构造函数和析构函数。**
1. **构造函数的功能是在创建对象时,给数据成员赋初值,即对象的初始化**。
1. **析构函数的功能是释放一个对象,在对象删除之前,用它来做一些内存释放等清理工作**
<a name="g73re"></a>
## 构造函数
```cpp
类内声明:
        类名(形参列表);
class Point{
private:
     int x,y;
public:
     Point();
     //……
};

类外实现:(需要加作用域)
    类名::类名(形参列表){  
         // 函数语句;
    }
Point:: Point()
{
    x=0;
    y=0;
}

特点:

  1. 构造函数的名字必须与类名相同
  2. 构造函数可以有任意类型的参数,但是没有返回值类型,也不能指定为void类型。
  3. 定义对象时,编译系统会自动地调用构造函数
  4. 通常构造函数被定义在公有部分
  5. 如果没有定义构造函数,系统会自动生成一个缺省的构造函数,只负责对象的创建,不做任何初始化工作。(无参构造函数,不做任何初始胡)
  6. 构造函数可以重载 ```cpp 1、在实际应用中,通常需要给每个类定义构造函数,如果没有给类定义构造函数,则系统自动生成一个默认的构造函数。 类名∷构造函数名( ){ }

2、构造函数不带参数 (无参构造) class Point{ private: int x,y; public: Point(){ 不带参数构造函数 x=0; y=0; } };

3、构造函数也可采用构造初始化表对数据成员进行初始化(有参构造) 类名∷构造函数名(实参表){ } class Date { private: int year,month,day; public: Date(int y,int m,int d):year(y),month(m),day(d){} 带参数构造函数 //构造函数初始化表对数据成员进行初始化 //…… };

4、如果数据成员是数组,则应在构造函数中使用相关语句进行初始化 class Student{ private: char name[10]; int age; public: Student(char na[],int a) { strcpy(name,na); //name是字符数组,所以用strcpy函数进行初始化 } //…… };

<a name="y3aVE"></a>
## 析构函数
**对象生命周期即将结束时,系统自动调用析构函数,完成清理工作**。<br />定义析构函数的一般格式:<br />**~类名( );**<br />**特点 **

1. 析构函数名是由**“~”和类名组成**的。
1. 析构函数**没有参数、也没有返回值**,而且也不能重载。
1. 通常析构函数被**定义在公有部分**,并由系统**自动调用**。
1. 一个类中**有且仅有一个析构函数**,且应为public。 
1. 析构函数**不仅可以在类内定义,也可以在类外定义**。
1. 如果**不定义,系统会自动生成一个默认的析构函数**“类名::~类名(){}”
1. 析构函数的**功能是释放对象所占用的内存空间,在对象生命周期结束前由系统自动调用**。
1. 析构函数与构造函数两者的调用次序相反。多个对象,**先构造的后析构**。
```cpp
[例3-11]   构造函数与析构函数的执行顺序——Point类的多个对象的创建与释放 
[例3-12]  对象定义在函数体内,析构函数的执行情况 
[例3-13]  复合语句中对象的析构函数的执行情况

四、内存动态分配的概念和使用方法

内存动态分配

用户存储区空间分三个部分:
程序区(代码区)、静态存储区(数据区)和动态存储区(栈区和堆区)

  1. 代码区存放程序代码,程序运行前就分配存储空间。
  2. 数据区存放常量、静态变量、全局变量等。
  3. 栈区存放局部变量、函数参数、函数返回值和临时变量等。
  4. 堆区是程序空间中存在的一些空闲存储单元,这些空闲存储单元组成堆。
  5. C++中使用new和delete来实现在堆内存区中进行数据的动态分配和释放

    运算符new

    在C++程序中,运算符new的功能是实现内存的动态分配。new运算符的使用格式包括三种形式:
    指针变量=new T;
    指针变量=new T(初始值列表);
    指针变量=new T [元素个数] ;
    T是一个数据类型名,例如:
    int p=new int(100); //让p指向一个类型为整型的堆地址,该地址中存放值100
    float
    p1=new float; //让p1指向一个类型为实型的堆地址

    new
     1、用new创建堆对象的格式:
         类名 *指针名= new 类名(构造函数参数)
             例:Complex *c1=new Complex(1.1,2.2);//创建对象*c1,并调用构造函数初始化数据成员real、imag为1.1、2.2 
    
     2、new后面的类是否跟参数取决于构造函数是否含参
     class Date{
     private:
         int year,month,day;
     public:
         Date(){
         year=2001; month=12; day=3;    
         } 
         ……
     }; 
     int main(){
         Date *today=new Date;
         ……
     }
    
     3、new 返回一个指定的合法数据类型的内存空间的首地址(指针),若分配不成功,则返回一个空指针。 
     4、new 可以为数组动态分配内存空间,这时应该在类型名后面指明数组大小。其中,元素个数是一个整型数值,可以是常数也可以是变量。
         例如: 
             int n=10,*p;
             p=new int[n];  表示new为具有n个元素的整型数组分配了内存空间,并将首地址赋给了指针p 
     5、new 不能对动态分配的数组存储区进行初始化。例如: int *p;
         p=new int[10](0);     错误,不能对动态分配的数组进行初始化
     6、用new分配的空间,使用结束后只能用delete显式地释放,否则这部分空间将不能回收而造成内存泄露
    delete
     1、运算符delete用来释放动态变量或动态数组所占的内存空间。
         格式:
             delete  指针变量名;
             delete[] 指针变量名;
    
     2、释放动态变量所占的内存空间,例如:
          int *p=new int;
          delete p;   //释放指针p所指向的动态内存空间
     3、释放动态数组所占的内存空间,例如:
         int *p;
         p=new int[10];
         delete []p;  //释放为数组动态分配的内存 
    说明 
     1、new和delete需要配套使用,如果搭配错了,程序运行时将会发生不可预知的错误。
     2、在用delete 释放指针所指的空间时,必须保证这个指针所指的空间是用new申请的,并且只能释放一次,否则将产生指针悬挂问题(见第六章)。
     3、如果在程序中用new申请了空间,就应该在结束程序前释放所有申请的空间。这样才能保证堆内存的有效利用。
     4、当delete用于释放由new创建的数组的连续内存空间时,无论是一维数组还是多维数组,指针变量名前必须使用[ ],且[ ]内没有数字。 
     [例3-14]  动态创建类Point的对象
    

    ```cpp

    include

    1.malloc() void malloc(size_t size) 1).malloc()函数会向堆中申请一片连续的可用内存空间 2).若申请成功 ,,返回指向这片内存空间的指针 ,若失败 ,则会返回NULL, 所以我们在用malloc()函数开辟动态内存之后, 一定要判断函数返回值是否为NULL. 3).返回值的类型为void型, malloc()函数并不知道连续开辟的size个字节是存储什么类型数据的 ,所以需要我们自行决定 ,方法是在malloc()前加强制转 ,转化成我们所需类型 ,如: (int)malloc(sizeof(int)n). 4).如果size为0, 此行为是未定义的, 会发生未知错误, 取决于编译器

例: int p = NULL; int n = 0; scanf(“%d”, &n); p = (int)malloc(sizeof(int) * n); if(p != NULL){ //….需要进行的操作 } 这时就相当于创建了一个数组 p[n] ,这个n的值并不需要像定义一个普通数组一样必须是常量, 可以使程序运行时得出的, 或是用户输入的

2.free() void free(void* ptr) 在堆中申请的内存空间不会像在栈中存储的局部变量一样,函数调用完会自动释放内存, 如果我们不手动释放, 直到程序运行结束才会释放, 这样就可能会造成内存泄漏, 即堆中这片内存中的数据已经不再使用, 但它一直占着这片空间, (通俗说就是就是占着茅坑不拉屎), 所以当我们申请的动态内存不再使用时 ,一定要及时释放 .

1).如果ptr没有指向使用动态内存分配函数分配的内存空间,则会导致未定义的行为。
2).如果ptr是空指针,则该函数不执行任何操作。
3).此函数不会更改ptr本身的值,因此它仍指向相同(现在已经无效)的位置(内存)
4).在free()函数之后需要将ptr再置空 ,即ptr = NULL;如果不将ptr置空的话 ,后面程序如果再通过ptr会访问到已经释放过无效的或者已经被回收再利用的内存, 为保证程序的健壮性, 一般我们都要写ptr = NULL; . 

注意 : free()不能重复释放一块内存, 如: free(ptr); free(ptr);是错的, 已经释放过的内存不能重复释放, 会出现内存错误

free()具体用法:

int *p = NULL;
int n = 0;
scanf("%d", &n);
p = (int*)malloc(sizeof(int) * n);
if(p != NULL){
    //....需要进行的操作
}
//操作完成 ,不再使用这片内存空间
free(p);
p = NULL;
<a name="F0WsU"></a>
# 五、掌握对象数组和对象指针
<a name="zl9hd"></a>
## 对象数组 
数组的元素可以是基本数据类型的数据,也可以是用户自定义数据类型的数据。对象数组是指每一个数组元素都是对象的数组。对象数组的元素是对象,它不仅具有数据成员,而且还有成员函数。
<a name="Lyza8"></a>
### 形式            
类名  数组名[下标表达式];        //声明<br />数组名[下标].成员函数            //对象数组引用<br />例如:                        <br />Circle c[3];        //定义类对象数组                <br />c[0].Area()        //调用公有成员函数
```cpp
1、求圆的面积
#include<iostream>
using namespace std;
class circle{
private:
    double radius;
public:
    circle(double r);
    double area();
    ~circle();
};
circle::circle(double r)
{
    cout<<"construct..."<<endl;
    radius=r;
}
 double circle::area()
{
    return radius*radius*3.14;
}
circle::~circle()
{
    cout<<"destruct..."<<endl;
}
int main()
{
    circle ff[3]={1,3,5};
    for(int i=0;i<3;i++)
    {
        cout<<ff[i].area()<<endl;
    }
    return 0;
}
输出结果:
construct…            //构造函数调用                                                    
construct…
construct…
3.14
28.26
78.5
destruct…            //析构函数调用
destruct…
destruct…

2、输出若干个平面上的点
#include<iostream>
using namespace std;
class point{
private:
    int x,y;
public:
    point(int a,int b);
    ~point();
    void print();
};
point::point(int a,int b)
{
    cout<<"constructing..."<<endl;
    x=a;
    y=b;
}
void point::print()
{
    cout<<"("<<x<<","<<y<<")"<<endl;
}
point::~point()
{
    cout<<"destructing..."<<endl;
}
int main()
{
    point ob[3]={point(1,2),point(3,4),point(5,6)};
    for(int i=0;i<3;i++){
        cout<<"第"<<i+1<<" 个点的坐标";
        ob[i].print();
    }
    return 0;
}
运行结果:
constructing...
constructing...
constructing...
第1 个点的坐标(1,2)
第2 个点的坐标(3,4)
第3 个点的坐标(5,6)
destructing...
destructing...
destructing...

对象指针

访问一个对象既可以通过对象名访问,也可以通过对象地址访问。对象指针就是用于存放对象地址的变量。
对象指针遵循一般变量指针的各种规则

  1. 声明对象指针的一般语法形式:类名* 对象指针名;
  2. 使用对象指针也可以访问对象的成员:对象指针名->成员名
  3. 与一般变量指针一样,对象指针在使用前必须先进行初始化可以让它指向一个已经声明过的对象,也可以用new运算符动态建立堆对象 ```cpp Circle c1,c(3); c1=&c;; c1-> Area (); //正确,c1在使用之前已指向一个已经声明过的对象 等价于(c1).Area() Circle c2=new Circle(3); c2-> Area (); //正确,c2在使用之前已利用new运算符 动态建立堆对象c2 Circle c3; c3-> Area (); //错误,不能使用没有初始化的对象指针

include

using namespace std; class circle{ private: double radius; public: circle(double r); double area(); ~circle(); }; circle::circle(double r) { cout<<”construct…”<area()<<endl; } return 0; } 输出结果: construct… //构造函数调用
construct… construct… 3.14 28.26 78.5 destruct… //析构函数调用 destruct… destruct…

int main() { circle *p=new circle(3); cout<<”圆的面积:”<area()<<endl; delete p; return 0; } construct… 圆的面积:28.26 destruct…

<a name="zEHhk"></a>
## 自引用指针this

- 当定义了一个类的若干对象后,每个对象都有属于自己的数据成员,而同一类的不同对象将共同拥有一份成员函数的拷贝,那么在执行不同对象所对应的成员函数时,各成员函数是如何分辨出当前调用自己的是哪个对象呢?

**当调用这个成员函数时,编译器它会向形参this传递调用成员函数的对象的地址。**

- 使用this指针时应该注意以下几点:
   - **this指针是一个const指针**,不能在程序中修改它或给它赋值。
   - this指针是一个局部数据,它的作用域仅在一个对象的内部。
   - **静态成员函数不属于任何一个对象。在静态成员函数中没有this指针。 **
```cpp
#include <iostream>
using namespace std;

class Square
{
private:
    double a;
public:
    Square(double a);
    double Area();
    void copy(Square &s);
};

Square::Square(double a)
{
    this->a = a;
}

double Square::Area()
{
    return a*a;
}

void Square::copy(Square &s)    &为引用
{
    if(this == &s)                &为取地址、判断是否为自己给自己赋值
        return;
    *this = s;                    将s的值赋值给this指向的对象s1
}

int main()
{
    Square s1(3),s2(5);
    cout << "before copy" <<endl;
    cout << "s1 area is " << s1.Area() <<endl;
    cout << "after copy" <<endl;
    s1.copy(s2);
    cout << "s1 area is " << s1.Area() <<endl;
}