您现在的位置是:首页 >技术杂谈 >learn C++ NO.3 ——类和对象(1)网站首页技术杂谈

learn C++ NO.3 ——类和对象(1)

玩铁的sinZz 2023-05-10 22:30:04
简介learn C++ NO.3 ——类和对象(1)

1.初步理解面向过程和面向对象

C语言是面向过程的高级编程语言,而C++是面向对象的高级编程语言。那么两者有什么区别呢?且看下图分析。
在这里插入图片描述
面向过程语言就是逐步拆分并解决问题。其特点是过程化和模块化,数据和对数据的操作是分离的。 由于面向过程不需要封装类再实例化调用,只用定义函数和调用,执行效率高一些。但是,有不易维护的缺点。
在这里插入图片描述
面向对象语言更注重对象之间的交互。生活中,人只需要将洗衣液和衣服丢给洗衣机,并按下按钮。至于洗衣机是怎么实现洗涤、脱水等功能,我们并不关心。因为这些功能的实现已经封装成了一个个按钮来方便我们的使用。

2.类的概念

在C语言中结构体变量内部只能定义成员变量,而在C++中结构体被升级成了类,结构体内不仅可以定义成员变量,还可以定义成员函数。下面我就以栈这个数据结构举一个例子。

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

using namespace std;

typedef int STDataType;

struct Stack
{
    void STInit(int defaultcapacity = 4)
    {
        STDataType* tmp = (STDataType*)malloc(sizeof(STDataType) * defaultcapacity);
        if (nullptr == tmp)
        {
            perror("malloc fail
");
            return;
        }

        data = tmp;
        top = 0;
        capacity = 4;
    }

    void STPush(STDataType x)
    {
        assert(data);
        if (capacity == top)
        {
            STDataType* tmp = (STDataType*)realloc(data, sizeof(STDataType) * capacity * 2);
            if (nullptr == tmp)
            {
                perror("realloc fail
");
                return;
            }

            data = tmp;
            capacity *= 2;
        }

        data[top++] = x;

    }

    bool STEmpty()
    {
        assert(data);
        return top == 0;
    }

    void STPop()
    {
        assert(data);
        assert(!STEmpty());
        top--;
    }

    STDataType STTop()
    {
        assert(data);
        assert(!STEmpty());

        return data[top - 1];
    }

    STDataType* data;
    int top;
    int capacity;
};

int main()
{
    Stack st;
    st.STInit();
    st.STPush(1);
    st.STPush(2);
    st.STPush(3);
    st.STPush(4);

    while (!st.STEmpty())
    {
        cout << st.STTop() << endl;
        st.STPop();
    }

    return 0;
}

当然,C++开发者更喜欢用类来定义自定义类型,即class

3.类的定义

3.1.语法

class 类名
{
	//类体即,成员函数和成员变量
};//分号不能丢

class为定义类的关键字,{}内部的是类的主体部分,类体也被称为类的成员。类中的变量被称为类的属性或成员变量,类中的函数被称为类的方法或成员函数。

3.2.类的两种定义方法

声明定义不分开

上面的栈的定义就是一种经典的声明和定义都在类里面定义的一种方法,需要注意的是,在类里面直接定义的函数可能会被编译器当成内联函数。这里就简单举个例子

class StuList
{
public:
	void Print()
	{
		cout << _id << "" << _name << "" << _score<<endl;
	}
private:
	int _id;
	char _name[8];
	int _score;
};

声明定义分开

在项目开发时,推荐使用定义和声明分开写。这样不仅能提高可读性,也可以对实现细节的隐藏。

//放在.h文件中
class StuList
{
public:
	void Print();//声明
private:
	int _id;
	char _name[8];
	int _score;
};
//.cpp文件中

#include"自己写的.h文件"

void StuList::Print()//需要在函数明前+(类名::)
{
		cout << _id << "" << _name << "" << _score<<endl;
}

3.3.类的成员变量命名规则

建议在类的成员变量命名时,采用特定的前缀或后缀以作区分。否则代码的可读性将大打折扣。

//不太规范的写法
class Date
{
public:
	void Init(int year = 2022, int month = 5, int day = 4)
	{
		year = year;
		month = month;
		day = day;
	}
private:
	int year;
	int month;
	int day;
};
//规范写法(前缀后缀具体还是看实际需求)

class Date
{
public:
	void Init(int year = 2022, int month = 5, int day = 4)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

3.类的访问限定符

C++实现类的实现将类的成员变量和成员函数结合到一起,这样可以使实例化出的对象更加完善。通过访问权限符,即可选择性的将其接口提供给外部的用户使用。在C++中,访问限定符有三种:public、protected和private。它们用于控制类中成员的访问权限。
在这里插入图片描述
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。class的默认访问限定符为private,而struct的默认访问限定符为public(C++需要兼容C)。

补充

访问限定符只在编译时有用,但是它们在程序执行期间仍然具有重要的作用。在编译期间,访问限定符可以防止程序员对数据进行无意义的操作,而在运行时,访问限定符可以防止程序员在不应该访问数据的情况下意外地访问数据。此外,访问限定符可以帮助编译器进行更好的优化,以提高程序的性能。因此,访问限定符对于保护代码的安全性和优化程序的性能都是非常重要的。

4.class和struct的区别

C++需要兼容C语言,struct的默认访问限定符为public,而class的默认访问限定符为private。当然用结构体定义类和class定义类是一样的。struct可以理解成一个轻量级的类,用于描述一些简单的数据结构,而class则更适合于实现复杂的面向对象的程序设计。

5.类的封装

类的封装就是一种管理,是为了让使用者更方便的使用类。比如,我们购买的笔记本电脑,厂商需要将cpu、内存、显卡、屏幕等封装起来,并让这些硬件正常的工作。而普通用户并不关心硬件是如何工作的。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互即可。
在这里插入图片描述
在这里插入图片描述
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。

6.类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作
符指明成员属于哪个类域。

class Person
{
public:
	void PrintPersonInfo();
private:
	char _name[20];
	char _gender[3];
	int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
	cout << _name << " " << _gender << " " << _age << endl;
}

7.类的实例化

概念

类的实例化就是用类创建对象的过程。类其实就是我们建造房子的图纸,而对象就是我们根据图纸建起来的一栋栋房子。只有对象实例化,才会在内存中开辟空间来存储成员变量,而类的声明不在内存中开辟空间存储成员变量名。这就像只有房子才能住人,而图纸无法住人。一个类可以实例化多个对象

class Person//该类是不占用空间的
{
public:
	char _name[20];
	char _gender[3];
	int _age;
};

int main()
{
	Person P1;//实例化出对象才占用内存空间
	P1._name = "zhangsan";
	P1._gender = "man";
	p1._age = 18;
	return 0;
}

8.类对象模型

8.1.计算类对象的大小

class A1
{
public:
    void Func1()
    {
    	cout << _a << endl;
    }
private:
    int _a;
};

int main()
{
	A1 aa;
	cout << sizeof(A1)<<endl;
	cout << sizeof(aa)<<endl;
	return 0;
}

在这里插入图片描述
可以看到其实类对象开辟空间时,类的成员函数是不占用类的空间。这就类的成员变量就像是厨房、卧室等,每家每户都需要这些基础的设施。而类的成员函数就像是健身房、篮球场等,通常都是建设在公共区域供大家一起使用。若每家每户都建了独立的篮球场或者健身房,这无疑是一种空间的浪费。那么空类是怎么计算大小的呢?

class A2
{
public:
    void Func2()
    {
        
    }
};

class A3
{
    
};

int main()
{
	cout << sizeof(A2)<<endl;
    cout << sizeof(A3)<<endl;
    return 0;
}

在这里插入图片描述
通过运行上面的代码可以发现,在C++中,当类的成员变量为空时,类的大小为一个字节,这是为了确保每个对象在内存中都有一个唯一标识的地址。实际,这一个字节的空间并不是用存储任何数据的,而是用对象的标识符。如果没有这个字节,两个不同的空对象的地址可能是相同的,这会导致一些问题。因此,编译器会在空类中插入一个字节的空间,以确保每个对象都有一个唯一的地址。

8.2.类和对象在内存中存储模型

在这里插入图片描述

8.3.结构体对齐规则

1、第一个成员变量在与结构体偏移量为0的地址处
2、其他成员变量对齐到自身的对齐数整数倍数和编译器默认的对齐数中,较小的值的整数倍数地址处。
3、结构体的总大小为:所有成员变量中的最大对齐数和编译器默认对齐数中取较小值为对齐数。对齐到该对齐数的整数倍处。
4、若结构体嵌套,则按照嵌套结构体对齐到自己最大对齐数的整数倍处。结构体的总大小取到对齐到最大对齐数(包含嵌套结构体)的整数倍处。
5、需要注意的是,结构体的大小可能会受到编译器的影响,因为编译器可能会对内存进行优化、对齐等处理。因此,在实际编程中,需要注意结构体的大小可能会发生变化。

8.4.结构体在内存中的模型

struct A1
{
    int _a;
    char _ch;
};

在这里插入图片描述

为什么要内存对齐呢?

因为内存对齐可以让CUP访问数据时,不会跨越多个存储单元,从而提高访问的效率。内存对齐还可以减少结构体对于内存空间的占用率,使得内存的利用率更高。

9.this指针

9.1.this指针概念

在C++中,this指针是一个指向当前对象的指针。它是一个隐藏的指针,在非静态成员函数中可以使用它来访问当前对象的成员变量和成员函数。当一个对象调用它的成员函数时,编译器会自动将对象的地址作为参数传递给这个成员函数,这个参数就是this指针。通过this指针,我们可以在成员函数内部访问调用该函数的对象的成员变量和成员函数。例如,可以使用this->variable 和 this->function() 访问当前对象的成员变量和成员函数。

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(2023,5,4);
	d2.Init(2023,5,5);
	d1.Print();
	d2.Print();
}

为什么d1.Print()函数就能访问到d1这个对象的数据呢?这是因为在C++中,调用成员函数时,编译器会隐式传递该对象的地址。这是编译器处理的。我就写一份编译器处理后的伪代码以便理解。

//这是一份伪代码,用于理解this指针
class Date
{
public:
	void Init(int year,int month,int day,Date* const this)
	{
		//this指针可以在成员函数内部使用
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	
	void Print(Date* const this)//C++语法规定,this指针不能作为函数的实参和形参显示传递或者接收
	{
	//this可以在成员函数内部使用
		cout << this->_year << " " << this->_month <<" " << this->_day << endl;
	}
	
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1,d2;
	d1.Init(2023,5,4,&d1);
	d2.Init(2023,5,5,&d2);
	d1.Print(&d1);
	d2.Print(&d2);
}

9.2.this指针的特性

1、this指针的类型为 类的类型名 + * const。所以成员函数中,不可以对this指针赋值。
2、只能在成员函数的内部使用。
3、this指针的本质是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
4、4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
在这里插入图片描述
所以,this指针是存储在栈区空间的。

9.3.this指针可以为空吗

// 1、下面程序的运行结果是?
// A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
    void Print()
    {

        cout << "Print()" << endl;
    }
private:
    int _a;
};

int main() 
{
    A* p = nullptr;
    p->Print();
    return 0;
}
// 2、下面程序的运行结果是?
// A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
    void Print()
    {

        cout << _a<< endl;
    }
private:
    int _a;
};

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

第一题的答案是正常运行,下面且听我的分析。首先,主函数内定义了一个A类型的空指针p,此时p是A类型对象的指针,通过p指针调用成员函数Print。此时p作为参数传给成员函数,并没有解引用操作。Print()函数也没有解引用操作。所以程序正常运行。
第二题的答案是程序崩溃。因为A的成员函数Print()内部,访问的是成员变量_a,此时产生了解引用操作。故程序崩溃。

10.c++和c语言实现栈的区别

c语言实现栈

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include<stdbool.h>

//数组默认容量
#define Def_Capacity 4

typedef int STDataType;

typedef struct Stack
{
    STDataType* data;
    int top;
    int capacity;
}ST;

//初始化栈
void STInit(ST* p)
{
	//判断指针有效性
    assert(p);
    STDataType* tmp=(STDataType*)malloc(sizeof(STDataType)*Def_Capacity);
    if(tmp == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    
    p->capacity =Def_Capacity;
    p->top = 0;
    p->data = tmp;
    
}

//销毁
void STDestroy(ST* p)
{
    assert(p);
    free(p->data);
    p->data = NULL;
    
    p->top = 0;
    p->capacity = 0;
}

//数据压栈
 void STPush(ST* p,STDataType x)
{
     assert(p);
     
     //检查容量
     if(p->capacity == p->top)
     {
         STDataType* tmp = (STDataType*)realloc(p->data,sizeof(STDataType)*p->capacity * 2 );
         if(tmp == NULL)
         {
             perror("realloc fail");
             exit(-1);
         }
         
         p->data = tmp;
         p->capacity *= 2;
         
     }
     //插入数据
     p->data[p->top++] = x;
 }
 
//判断是否为空栈
bool STEmpty(ST* p)
{
    assert(p);
    
    return p->top == 0;
    
}

//数据出栈
void STPop(ST* p)
{
    assert(p);
    //空栈不可删除
    assert(!STEmpty(p));

    p->top--;
    
}

//取栈顶元素
STDataType STTop(ST* p)
{
    assert(p);
    assert(!STEmpty(p));
    
    return p->data[p->top-1];
    
}

//栈的有效元素个数
int STSize(ST* p)
{
    assert(p);
    
    return p->top;
}

int main()
{
    ST st;
    
    STInit(&st);
    STPush(&st,1);
    STPush(&st,2);
    STPush(&st,3);
    STPush(&st,4);
    
    printf("top::%d
",STTop(&st));
    
    while(!STEmpty(&st))
    {
        printf("%d
",STTop(&st));
        STPop(&st);
    }
    
    return 0;
}

C语言实现栈是有以下特性:
1、每个函数的第一个参数的类型都是ST* 。
2、函数中都必须对指针做有效性检查,因为可能会有NULL指针问题。
3、函数中必需通过ST*类型的形参来操作栈。
4、传参调用时,必须传递结构体的地址。
结构体中只能存放结构体的成员变量,涉及操作数据结构的方法不能放在结构体中,即数据和操作数据的方法是分离开的,而且实现上相当复杂,涉及大量的指针操作和内存管理操作,稍有不慎就会导致程序出错。

c++语言实现栈

typedef int STDataType;

struct Stack
{
public:
    void STInit(int defaultcapacity = 4)
    {
        STDataType* tmp = (STDataType*)malloc(sizeof(STDataType) * defaultcapacity);
        if (nullptr == tmp)
        {
            perror("malloc fail
");
            return;
        }

        _data = tmp;
        _top = 0;
        _capacity = 4;
    }

    void STPush(STDataType x)
    {
        if (_capacity == _top)
        {
            STDataType* tmp = (STDataType*)realloc(_data, sizeof(STDataType) * _capacity * 2);
            if (nullptr == tmp)
            {
                perror("realloc fail
");
                return;
            }

            _data = tmp;
            _capacity *= 2;
        }

        _data[_top++] = x;

    }

    bool STEmpty()
    {
        return _top == 0;
    }

    void STPop()
    {
        _top--;
    }

    STDataType STTop()
    {
        return _data[_top - 1];
    }
    
    int STSize()
    {
        return _top;
    }
    
private:
    STDataType* _data;
    int _top;
    int _capacity;
};



int main()
{
    Stack st;
    st.STInit();

    st.STPush(1);
    st.STPush(2);
    st.STPush(3);
    st.STPush(4);

    while (!st.STEmpty())
    {
        cout << st.STTop() << endl;
        st.STPop();
    }

    return 0;
}

C++中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制那些方法在类外可以被 调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。而且每个方法不需要传 递Stack*的参数了,编译器编译之后该参数会自动还原,即C++中 Stack * 参数是编译器维护的,C语言中需 用用户自己维护。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。