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

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

目录

一.前言

二. 类的引入和定义

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

2.C++类的定义

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

三.面向对象之封装特性

1.封装思想的介绍

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

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

五.this指针

章节导图:

一.前言
面向过程和面向对象初步认识:

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

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

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

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

比如:

代码段1:

include

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

include <assert.h>

include

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

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

include <assert.h>

include

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

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

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

include <assert.h>

include

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;

}

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

相关文章
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
80 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
86 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
31 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
31 4
|
29天前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
101 13
|
6月前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
2月前
|
C语言
无头链表二级指针方式实现(C语言描述)
本文介绍了如何在C语言中使用二级指针实现无头链表,并提供了创建节点、插入、删除、查找、销毁链表等操作的函数实现,以及一个示例程序来演示这些操作。
36 0
|
3月前
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
127 4
|
4月前
|
C语言
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)