首页 > 编程语言 > 详细

C++ 基础 3:类和对象

时间:2020-07-02 18:42:15      阅读:46      评论:0      收藏:0      [点我收藏+]

1 基本概念

1.1 类定义

类定义是以关键字 class 开头,后跟类的名称。类的主体是包含在一对花括号中。类定义后必须跟着一个分号或一个声明列表。例如,我们使用关键字 class 定义 Box 数据类型,如下所示:

class Box
{
   public:
      double length;   // 盒子的长度
      double breadth;  // 盒子的宽度
      double height;   // 盒子的高度

      // 成员函数声明
      double getVolume(void);// 返回体积
};

1.2 C++ 对象

对象是根据类来创建的。声明类的对象,就像声明基本类型的变量一样。下面的语句声明了类 Box 的两个对象:

Box Box1;          // 声明 Box1,类型为 Box
Box Box2;          // 声明 Box2,类型为 Box

1.3 成员变量和成员函数

成员变量:BOX 类中的 length,breadth,height。

成员函数:getVolume();

成员函数可以定义在类定义内部,或者单独使用范围解析运算符 :: 来定义。

  1. 在类定义中定义的成员函数把函数声明为内联的,即便没有使用 inline 标识符。如下所示:
class Box
{
   public:
      double length;      // 长度
      double breadth;     // 宽度
      double height;      // 高度
   
      double getVolume(void)
      {
         return length * breadth * height;
      }
};
  1. 也可以在类的外部使用范围解析运算符 :: 定义该函数,如下所示:
double Box::getVolume(void)
{
    return length * breadth * height;
}

2 对象的构造和析构

2.1 构造函数

C++ 中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数。

class 类名
{
    类名(形参)
    {
        构造体
    }
}

2.2 析构函数

C++ 中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数。

class 类名
{
    ~类名(形参)
    {
        析构体
    }
}

2.3 构造函数的分类

2.3.1 无参构造函数

代码示例:

// strucFunc1.cpp,无参构造函数

#include <iostream>

using namespace std;

class Test
{
	public:

		// 无参构造函数
		Test()
		{
			a = 0;
			b = 0;

			cout << "Test() 无参构造函数执行" << endl;
		}

	private:

		int a;
		int b;
};

int main()
{
	Test t; // 调用无参构造函数

	getchar();

	return 0;
}

运行结果:

技术分享图片

2.3.2 有参构造函数

代码示例:

// strucFunc2.cpp,有参构造函数

#include <iostream>

using namespace std;

class Test
{
	private:

		int a;

	public:

		// 有参构造函数
		Test(int a)
		{
			cout << "a = " << a << endl;
		}

		// 有参构造函数
		Test(int a,int b)
		{
			cout << "a = " << a << ", b = "<< b << endl;
		}
};

int main()
{
	Test t1(1); // 调用有参构造函数Test(int a)
	Test t2(2,3); // 调用有参构造函数Test(int a,int b)

	getchar();

	return 0;
}

运行结果:

技术分享图片

2.3.3 拷贝构造函数

本小节内容参考 AlanTu:C++ 拷贝构造函数详解

对于普通类型的对象来说,他们之间的复制是很简单的,如:

int a = 1;
int b = a;

而类对象与普通对象不同,类对象内部结构存在各种成员变量。因此需要使用拷贝构造函数。

拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量

拷贝构造函数的最常见形式如下:

class 类名
{
      类名(const 类名 & another)
      {
            拷贝构造体
      }
}

2.3.3.1 拷贝构造函数被调用的场景

1. 通过使用另一个同类型的对象来初始化新创建的对象
// CopyConstructorTest1.cpp,拷贝构造函数被调用的场景: 1. 通过使用另一个同类型的对象来初始化新创建的对象

#include <iostream>

using namespace std;

class CopyConstructorTest1
{
	private:

		int a;

	public:

		// 构造函数
		CopyConstructorTest1(int temp)
		{
			a = temp;

			cout << "构造函数被调用" << endl;
		}

		// 拷贝构造函数
		CopyConstructorTest1(const CopyConstructorTest1 & tempClass)
		{
			a = tempClass.a;

			cout << "拷贝构造函数被调用" << endl;
		}

		// 析构函数
		~CopyConstructorTest1()
		{
			cout << "析构函数被调用" << endl;
		}

		void show()
		{
			cout << "a = " << a << endl;
		}
};

int main()
{
	CopyConstructorTest1 A(1); 
	CopyConstructorTest1 B = A;  // 1 通过使用另一个同类型的对象来初始化新创建的对象。

	B.show();

	return 0;
}

运行结果:

技术分享图片

2. 当函数的参数为类的对象时
// CopyConstructorTest2.cpp,拷贝构造函数被调用的场景: 2. 当函数的参数为类的对象时

#include <iostream>

using namespace std;

class CopyConstructorTest1
{
	private:

		int a;

	public:

		// 构造函数
		CopyConstructorTest1(int temp)
		{
			a = temp;

			cout << "构造函数被调用" << endl;
		}

		// 拷贝构造函数
		CopyConstructorTest1(const CopyConstructorTest1 & tempClass)
		{
			a = tempClass.a;

			cout << "拷贝构造函数被调用" << endl;
		}

		// 析构函数
		~CopyConstructorTest1()
		{
			cout << "析构函数被调用" << endl;
		}

		void show()
		{
			cout << "a = " << a << endl;
		}
};

void funtionTest(CopyConstructorTest1 tempClassCopyConstructorTest1)
{
	cout << "funtionTest 函数被调用" << endl;

	tempClassCopyConstructorTest1.show();
}

int main()
{
	CopyConstructorTest1 A(1); 
	
	funtionTest(A); // 2. 当函数的参数为类的对象时,会调用拷贝构造函数

	return 0;
}

运行结果:

技术分享图片

funtionTest() 函数执行时,会执行以下步骤:

  1. A 对象传入 funtionTest() 函数时,会产生形参 tempClassCopyConstructorTest1

  2. 调用拷贝构造函数把 A 对象的值给 tempClassCopyConstructorTest1

1.2 步 总和起来类似于 CopyConstructorTest1 tempClassCopyConstructorTest1 = A;

  1. funtionTest() 函数执行完后,析构掉 tempClassCopyConstructorTest1
3. 函数的返回值是类的对象
// CopyConstructorTest3.cpp,拷贝构造函数被调用的场景: 3. 函数的返回值是类的对象

#include <iostream>

using namespace std;

class CopyConstructorTest1
{
	private:

		int a;

	public:

		// 构造函数
		CopyConstructorTest1(int temp)
		{
			a = temp;

			cout << "构造函数被调用" << endl;
		}

		// 拷贝构造函数
		CopyConstructorTest1(const CopyConstructorTest1 & tempClass)
		{
			a = tempClass.a;

			cout << "拷贝构造函数被调用" << endl;
		}

		// 析构函数
		~CopyConstructorTest1()
		{
			cout << "析构函数被调用" << endl;
		}

		void show()
		{
			cout << "a = " << a << endl;
		}
};

CopyConstructorTest1 funtionTest()
{
	cout << "funtionTest 函数被调用" << endl;

	CopyConstructorTest1 A(1);

	A.show();

	return A;
}

int main()
{
	funtionTest(); // 3. 函数的返回值是类的对象

	return 0;
}

运行结果:

技术分享图片

funtionTest()函数执行时,会执行以下步骤:

  1. 调用构造函数创建 A 对象

  2. 编译器内部产生一个临时变量 X(看不见)

  3. 调用拷贝构造函数把 对象 A 的值给 对象 X

  4. 析构 A

  5. 析构 X

2.4 浅拷贝与深拷贝

本小节内容参考 AlanTu:C++ 拷贝构造函数详解

2.4.1 默认拷贝构造函数

很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式:

Rect::Rect(const Rect& r)
{
    width = r.width;
    height = r.height;
}

以上代码不用我们编写,编译器会为我们自动生成。但是如果认为这样就可以解决对象的复制问题,那就错了,让我们来考虑以下一段代码:

// CopyTest1.cpp,默认构造函数的局限性

#include <iostream>

using namespace std;

class Rect
{
	public:

		Rect()
		{
			count++;
		}

		~Rect()
		{
			count--;
		}

		static int getCount()
		{
			return count;
		}

private:

    int width;
    int height;

    static int count;
};

int Rect::count = 0;

int main()
{
    Rect rect1;

    cout << "The count of Rect:" << Rect::getCount() << endl;

    Rect rect2(rect1);

    cout << "The count of Rect:" << Rect::getCount() << endl;

    return 0;
}

运行结果:

技术分享图片

这段代码对前面的类,加入了一个静态成员,目的是进行计数。在主函数中,首先创建对象rect1,输出此时的对象个数,然后使用rect1复制出对象rect2,再输出此时的对象个数,按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。

说白了,就是拷贝构造函数没有处理静态数据成员。

2.4.2 浅拷贝

所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让我们考虑如下一段代码:

#include <iostream>
#include <assert.h>

using namespace std;

class Rect
{
    public:

        Rect()
        {
             p = new int(100);
        }
   
        ~Rect()
        {
            assert(p != NULL);
            delete p;
        }

    private:
        int width;
        int height;
        int *p;
};

int main()
{
    Rect rect1;

    Rect rect2(rect1);

    return 0;
}

在这段代码运行结束之前,会出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作。我们来分析一下:

在运行定义 rect1 对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况大致如下:

技术分享图片

在使用 rect1 复制 rect2 时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时 rect1.p = rect2.p,也即这两个指针指向了堆里的同一个空间,如下图所示:

技术分享图片

当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。

2.4.3 深拷贝

在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:

#include <iostream>
#include <assert.h>

using namespace std;

class Rect
{
    public:
        
        Rect()
        {
             p = new int(100);
        }
    
        Rect(const Rect& r)
        {
            width = r.width;
            height = r.height;

            p = new int(100);

            *p = *(r.p);
        }
     
        ~Rect()
        {
            assert(p != NULL);
            delete p;
        }

private:

        int width;
        int height;
        int *p;
};

int main()
{
    Rect rect1;
    Rect rect2(rect1);

    return 0;
}

此时,在完成对象的复制后,内存的一个大致情况如下:

技术分享图片

此时 rect1 的 p 和 rect2 的 p 各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。

2.4.4 总结

拷贝有两种:深拷贝,浅拷贝。

当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。

深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。

浅拷贝只拷贝栈上的变量,深拷贝同时要拷贝堆上面的内存。

3 对象动态建立 new 和 释放 delete

了解动态内存在 C++ 中是如何工作的是成为一名合格的 C++ 程序员必不可少的。C++ 程序中的内存分为两个部分:

  • 栈:在函数内部声明的所有变量都将占用栈内存。

  • 堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存。

很多时候,您无法提前预知需要多少内存来存储某个定义变量中的特定信息,所需内存的大小需要在运行时才能确定。

在 C++ 中,您可以使用特殊的运算符为给定类型的变量在运行时分配堆内的内存,这会返回所分配的空间地址。这种运算符即 new 运算符。

如果您不再需要动态分配的内存空间,可以使用 delete 运算符,删除之前由 new 运算符分配的内存。

C++ 提供了运算符 new 和 delete 来取代 malloc 和 free 函数。

下面是使用 new 运算符来为任意的数据类型动态分配内存的通用语法:

new data-type;

例如,我们可以定义一个指向 double 类型的指针,然后请求内存,该内存在执行时被分配。我们可以按照下面的语句使用 new 运算符来完成这点:

double* pvalue  = NULL;
if( !(pvalue  = new double ))
{
   cout << "Error: out of memory." <<endl;
   exit(1);
 
}

在任何时候,当您觉得某个已经动态分配内存的变量不再需要使用时,您可以使用 delete 操作符释放它所占用的内存,如下所示:

delete pvalue; // 释放 pvalue 所指向的内存

4 静态成员变量和成员函数

类的静态成员,属于类,也属于对象,但终归属于类。

4.1 静态成员变量

// 声明
static 数据类型 成员变量; // 在类的内部

// 初始化
数据类型 类名::静态数据成员 = 初值; // 在类的外部

// 调用
类名:: 静态数据成员
类对象.静态数据成员
  1. static 成员变量实现了同类对象间信息共享

  2. static 成员类外存储,求类大小并不包含在内

  3. static 成员是命名空间属于类的全局变量,存储在 data 区

  4. static 成员只能类外初始化

  5. static 成员 可以通过类名访问,也可以通过对象访问

4.2 静态成员函数

// 声明
static 函数声明

// 调用
类名::函数调用
类对象.函数调用
  1. 静态成员函数的意义,不在于信息共享,数据沟通,而在于管理静态数据成员,完成对静态数据成员的封装。

  2. 静态成员函数只能访问静态数据成员。原因:非静态成员函数,在调用时 this 指针被当做参数传进,而静态成员函数属于类,不属于对象,没有 this 指针。

C++ 基础 3:类和对象

原文:https://www.cnblogs.com/PikapBai/p/13205132.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!