C++类和对象(上): 封装与this指针

目录

一.前言

二. 类的引入和定义

1.C和C++结构体的区别

2.C++类的定义

3.类的成员方法的声明和定义是可分离的

三.面向对象之封装特性

1.封装思想的介绍

2.类封装编程模式的优点

四. 类实例(对象)的内存模型

五.this指针


章节导图: 

一.前言

面向过程和面向对象初步认识:

1.C语言是面向过程的,关注的是过程分析出求解问题的步骤,通过函数调用逐步解决问题

2.C++是基于面向对象的,关注的是对象将一件事情拆分成不同的对象,靠对象之间的交互完成

对于面向对象的编程思想,我们还需要长时间的学习和积累才可能对其有更深刻的理解。

二. 类的引入和定义

1.C和C++结构体的区别

C语言结构体中只能定义变量在C++中,结构体内不仅可以定义变量,也可以定义函数。(可在其中定义函数的自定义类型为面向对象编程提供了可能)

比如:

代码段1:

#include <iostream>
using std::cout;
using std::cin;
using std::endl;
#include <assert.h>
#include <string>

struct STU
{                            
	int _data;                             为了区分结构中成员变量和结构函数的形参,习惯性地在成员变量前标记_
	char _name[20];
	void init(int data, const char* name)  //结构体初始化函数
	{
		assert(name);
		_data = data;                      结构体中的函数可以直接访问结构体的成员变量
		strcpy(_name, name);
	}
	 
	void Prin()                            //结构体打印函数
	{
		cout << _data << endl;
		cout << _name << endl;
	}
};


int main()
{
	STU a;                                  创建一个结构体变量(C++中类型名无需加struct关键字)
	a.init(23, "张三");
	a.Prin();
	return 0;
}

结构体中的函数可以直接访问结构体的成员变量

注意:

为了区分结构(类)中成员变量 和 结构(类)成员函数的形参,习惯性地在成员变量标识名前标记_(具体标记什么无所谓)。                                      

2.C++类的定义

C++的类也是一种类似于C++结构体的自定义类型(两者的区别后续学习中再进一步探究),不仅可以定义变量,也可以定义函数(类中的函数亦称为方法)

C++类定义:

class className
{                   // 类体:由成员方法和成员变量组成
    public:         // 公有域和私有域可以在类中随意划分
    //公有域的成员
    private:
    //私有域的成员
    public:
    //公有域的成员
    ......
    
};  // 一定要注意后面的分号

相关关键字和公私有域介绍:

(1)class为定义类的关键字,ClassName为类的名字,{}中为类的主体。

(2)类的私有域(由关键字private标识)中的成员只能被类中的方法访问,不能在类作用域(由类的花括号限定)外被访问

(3)类的公有域(由关键字public标识)的成员既可以被类中的方法访问,也可以在类作用域(由类的花括号限定)外被访问

C++类和结构体的其中一个区别在于成员变量和方法所在默认公私有域不同:

(class的默认外部访问权限为private,struct为public(因为struct要兼容C))

(1)结构体的成员变量和方法在用户不指定公私有域的情况下,默认属于public公有域

(2)类的成员变量和方法在用户不指定公私有域的情况下,默认属于private私有域.

(编程中最好不要使用默认的公私属性,要明确地划分好公有域和私有域)

但是在C++中,构建某类对象时我们一般习惯使用class而不是struct(并且class和struct在后续的深入学习中还有一些其他方面的差别)

3.类的成员方法的声明和定义是可分离的

类的成员方法的声明和定义是可以分离的,我们可以在类的作用域中只写成员方法的声明,然后成员方法的定义可以放在类的作用域外;

比如:

注意:在类的作用域外实现的成员方法标识名前一定要用类名加作用域限定符:: 来标识成员方法属于某个类的作用域。(类似于命名空间,类实质上定义了一个新的作用域)

这种类的实现方式在工程项目中十分有用,可以大大提高代码的可读性和可维护性。

补充:成员方法如果在类的作用域中完成定义,编译器可能会将其当成内联函数处理。内联函数参见:http://t.csdn.cn/r8m2N

三.面向对象之封装特性

1.封装思想的介绍

面向对象的封装思想:将数据和操作数据的方法进行有机结合,通过访问权限的选择限定隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。(类的语法特性就是为封装而设计的)

封装本质上是一种管理,让用户更便捷地使用类。

(打个比方:计算机出厂时,在外部套上壳子,将内部的电路,芯片等等细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等操作接口,让用户可以边界地与计算机进行交互)

2.类封装编程模式的优点

相比于C语言的编程模式(将函数模块暴露在全局作用域中) ,C++的类封装模式具有很多的优越性.

比如现在我们用类来实现一个栈对象。

using std::cin;
using std::cout;
using std::endl;


typedef int DataType;
class Stack
{
public:
    void Init()
    {
        _array = (DataType*)malloc(sizeof(DataType) * 3);
        if (NULL == _array)
        {
            perror("malloc申请空间失败!!!");
            return;
        }
        _capacity = 3;
        _size = 0;
    }
    

    void Push(DataType data)
    {
        CheckCapacity();
        _array[_size] = data;
        _size++;
    }



    void Pop()
    {
        if (Empty())
        return;
        _size--;
    }
    

    DataType Top()
    { 
        return _array[_size - 1];
    }



    int Empty() 
    { 
        return 0 == _size;
    }



    int Size()
    { 
        return _size;
    }

    void Destroy()
    {
        if (_array)
        {
            free(_array);
            _array = NULL;
            _capacity = 0;
            _size = 0;
        }
    }
private:
    void CheckCapacity()
    {
        if (_size == _capacity)
        {
            int newcapacity = _capacity * 2;
            DataType* temp = (DataType*)realloc(_array, newcapacity *
            sizeof(DataType));
            if (temp == NULL)
            {
                perror("realloc申请空间失败!!!");
                return;
            }
            _array = temp;
            _capacity = newcapacity;
        }
    }

private:
    DataType* _array;
    int _capacity;
    int _size;
};


int main()
{
    Stack s;
    s.Init();
    s.Push(1);
    s.Push(2);
    s.Push(3);
    s.Push(4);
    printf("%d\n", s.Top());
    printf("%d\n", s.Size());
    s.Pop();
    s.Pop();
    printf("%d\n", s.Top());
    printf("%d\n", s.Size());
    s.Destroy();
    return 0;
}

如果用C语言来实现栈:

typedef int DataType;
typedef struct Stack
{
    DataType* array;
    int capacity;
    int size;
}Stack;


void StackInit(Stack* ps)
{
    assert(ps);
    ps->array = (DataType*)malloc(sizeof(DataType) * 3);
    if (NULL == ps->array)
    {
        assert(0);
        return;
    }
    ps->capacity = 3;
    ps->size = 0;
}


void StackDestroy(Stack* ps)
{
    assert(ps);
    if (ps->array)
    {
        free(ps->array);
        ps->array = NULL;
        ps->capacity = 0;
        ps->size = 0;
    }
}


void CheckCapacity(Stack* ps)
{
    if (ps->size == ps->capacity)
    {
        int newcapacity = ps->capacity * 2;
        DataType* temp = (DataType*)realloc(ps->array,newcapacity*sizeof(DataType));
        
        if (temp == NULL)
        {
            perror("realloc申请空间失败!!!");
            return;
        }
        ps->array = temp;
        ps->capacity = newcapacity;
    }
}


void StackPush(Stack* ps, DataType data)
{
    assert(ps);
    CheckCapacity(ps);
    ps->array[ps->size] = data;
    ps->size++;
}

int StackEmpty(Stack* ps)
{
    assert(ps);
    return 0 == ps->size;
}

void StackPop(Stack* ps)
{
    if (StackEmpty(ps))
    return;
    ps->size--;
}


DataType StackTop(Stack* ps)
{
    assert(!StackEmpty(ps));
    return ps->array[ps->size - 1];
}


int StackSize(Stack* ps)
{
    assert(ps);
    return ps->size;
}


int main()
{
    Stack s;
    StackInit(&s);
    StackPush(&s, 1);
    StackPush(&s, 2);
    StackPush(&s, 3);
    StackPush(&s, 4);
    printf("%d\n", StackTop(&s));
    printf("%d\n", StackSize(&s));
    StackPop(&s);
    StackPop(&s);
    printf("%d\n", StackTop(&s));
    printf("%d\n", StackSize(&s));
    StackDestroy(&s);
    return 0;
}

C++的类封装写法相比于C语言将栈操作函数放置在全局区域而言的好处

(1).C++中通过类可以将数据以及操作数据的方法进行完美结合,将所有栈操作函数封装在类中便于代码的维护和管理

(2).由于类存在公私有访问权限的限定,所以外部用户无法修改栈对象的 _array ,_capacity,_size这三个维护栈的关键信息,而C语言在主函数中是可以随意对维护栈的指针和栈容量等信息进行修改的,因此C++的栈对象使用起来更加安全。

(3).将所有栈操作函数封装在类中,使用时统一通过类成员访问的方式去调用,代码的可读性更高(尤其是当同类型类对象很多的时候)

(4)C语言实现的栈涉及到大量指针操作,稍不注意可能就会出错。

(5)编程思维的转变:从编写功能模块去操作对象的编程思维转变成构造对象去使用自身成员方法的编程思维。

四. 类实例(对象)的内存模型

用定义好的类去创建类对象实例,对象实例在自身的内存区块中只存放了成员变量,类的成员方法(函数体指令段)存放在只读常量区被所有类实例对象所共用

类对象实例的内存区块中,只存有成员变量,而不存放函数体 

有两个例子可以验证这一点:

例一:

#include <iostream>
using std::cout;
using std::cin;
using std::endl;
#include <assert.h>
#include <string>

static class STU
{
                               
	int _data;                             //为了区分结构中成员变量和结构函数的形参,习惯性地在成员变量前标记_
	char _name[20];

public:

	void init(int data, const char* name)  //结构体初始化函数
	{
		assert(name);
		_data = data;                      //结构体中的函数可以直接访问结构体的成员变量
		strcpy(_name, name);
	}
	 
	void Prin()                            //结构体打印函数
	{
		cout << _data << endl;
		cout << _name << endl;
	}

	void Prin2()
	{
		cout << "Prin2" << endl;
	}
};


int main()
{
	STU a;                                  //创建一个类对象a
	STU b;                                  //创建一个类对象b
	a.init(23, "张三");
	b.init(24, "李四");
	return 0;
}

创建两个STU对象a,b,分别调用它们方法init,转到汇编代码观察两次调用init时call指令访问到的函数体地址:

这充分说明了多个类对象公用的是同一个函数体代码段。

例二:

代码段(1)

class A
{
public:
    void PrintA()
    {
        cout<<_a<<endl;
    }
private:
    int _a;
};


int main()
{
    A* p = nullptr;        将空指针赋给类指针p
    p->PrintA(); 
    return 0;
}

主函数中没有创建类对象实例,p是空指针(值为0),而PrintA函数中访问了类成员_a,所以调用PrintA方法会导致非法访问内存空间

代码段(2)

class A
{
public:
    void Print()
    {
        cout << "Print()" << endl;
    }
private:
    int _a;
};


int main()
{
    A* p = nullptr;
    p->Print();
    return 0;
}

代码段(2)的不同之处在于Print方法中没有访问类的成员变量,又因为类的方法(函数体)是存放在只读常量区的,并没有存放在任何类对象实例的内存空间中,所以该代码段可以正常运行输出。这也充分说明了类函数体的代码段是独立存放在内存中的。

因此,类对象实例的所占的内存空间根据其成员变量来计算即可,各成员变量的内存分布遵循结构体内存对齐原则.

结构体内存对齐参见:http://t.csdn.cn/eTtYF

五.this指针

1.C++编译器在编译阶段会给的每个非静态(非static修饰)的成员函数(方法)形参表中增加一个隐藏的指针参数(形参).(指针参数名为this,this在C++中被作为关键字)。(编译器添加this指针的操作不会在源码层面改变代码,仅仅只是在编译阶段临时添加而已)

2.类对象调用成员函数时,编译器会自动向成员函数传入当前对象的地址作为this指针的实参使this指针指向当前对象

3.在成员函数体(成员方法)中所有访问成员变量的操作,都是通过this指针实现的

4.this指针的类型为 : (类名)* const; this 指针被const保护无法被修改

比如:

using std::cout;
using std::cin;
using std::endl;
#include <assert.h>
#include <string>

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};


int main()
{
	Date d1, d2;
	d1.Init(2022, 1, 11);
	d2.Init(2022, 1, 12);
	d1.Print();
	d2.Print();
	return 0;
}

编译时:

 ​​

注意:this形参指针在各“成员函数”形参表的第一个位置
我们可以通过汇编来观察一下:

 继续往下调试通过call指令跳转到函数体指令段中

this指针的添加和使用的操作在源码层面是不可见的,都是编译器在编译阶段自动完成的。
但是通过汇编可以很清楚地看到this指针的存在。

this指针的存在使类成员方法可以返回类对象的地址或类对象本身的引用

比如:

#include <iostream>
using std::cout;
using std::cin;
using std::endl;
#include <assert.h>
#include <string>

class Date
{
public:
	Date& Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
		return *this;
	}
	Date* Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};


int main()
{
	Date d1;
	(d1.Init(2022, 1, 11)).Print();
	return 0;
}

 

这种用法在后续的学习中会经常见到,也十分地重要。

猜你喜欢

转载自blog.csdn.net/weixin_73470348/article/details/128746469