Bootstrap

C++:类和对象(上)

目录

一、类的定义

二、 访问限定符

三、 实例化概念类:

类(Class)

对象(Object)

实例化(Instantiation)

四、 对象大小

五、this 指针的基本概念

this 指针的作用:

this 指针的类型:

this 指针的隐式传递:


一、类的定义

1、class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。

类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。

class 类名 Stack {
    //访问修饰符:
        //成员变量;
        //成员函数;
};

2、为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前⾯或者后⾯加_或者m 开头。

class Stack {
private:
    int top_;  // 成员变量,私有
    int* data_;  // 成员变量,私有

public:
    Stack();  // 构造函数
    ~Stack();  // 析构函数

    void push(int value);  // 成员函数
    int pop();  // 成员函数
    bool isEmpty() const;  // 成员函数
};

3、 C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是 struct中可以定义函数,⼀般情况下我们还是推荐用class定义类。

4、 定义在类⾯的成员函数默认为inline。

class MyClass {
public:
    // 这个函数在类定义内部定义,因此默认是 inline
    void myFunction() {
        std::cout << "Hello, world!" << std::endl;
    }
};

int main() {
    MyClass obj;
    obj.myFunction(); // 调用可能会被内联
    return 0;
}

在这个例子中,myFunction 是在 MyClass 类的定义内部声明并定义的,所以它是隐式内联的。

但是,如果你在类外部定义成员函数,则需要显式地使用 inline 关键字来请求编译器将其作为内联函数处理,尽管这仍然是一个建议而非强制要求:
 

class MyClass {
public:
    void myFunction();
};

// 需要显式指定 inline
inline void MyClass::myFunction() {
    std::cout << "Hello, world!" << std::endl;
}

总结来说,C++ 中在类定义内部定义的成员函数默认为 inline 函数,这是一个方便的特性,可以提高代码的执行效率,特别是在处理小型、频繁调用的函数时。是否实际内联是由编译器决定的。

二、 访问限定符

• C++⼀种实现封装的方式,⽤类将对象的属性与方法结合在⼀块,让对象更加完善,通过访问权限选择性的将其接⼝提供给外部的用户使用。

public修饰的成员在类外可以直接被访问;protectedprivate修饰的成员在类外不能直接被访 问,protected和private是⼀样的,以后继承章节才能体现出他们的区别。

• 访问权限作用域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后面没有 访问限定符,作用域就到}即类结束。

• class定义成员没有被访问限定符修饰时默认为private,struct默认为public。

• ⼀般成员变量都会被限制为private/protected,需要给别⼈使一瓶的成员函数会放为public。。

class MyClass {
private:
    int privateVar;  // 私有成员变量
protected:
    int protectedVar;  // 受保护的成员变量
public:
    int publicVar;  // 公共成员变量

    void publicMethod() {  // 公共成员函数
        privateVar = 10;
        protectedVar = 20;
        publicVar = 30;
    }

    void protectedMethod() {  // 受保护的成员函数
        // 可以访问所有类型的成员
    }

private:
    void privateMethod() {  // 私有成员函数
        // 只能在类内部访问
    }
};

struct MyStruct {
    int var1;  // 默认为 public
private:
    int var2;  // 私有成员变量
protected:
    int var3;  // 受保护的成员变量
};

总结
public 成员可以在任何地方被访问。
protected 成员可以在类的内部及其派生类中被访问。
private 成员只能在类的内部被访问。
class 中的成员默认为 private,struct 中的成员默认为 public。
通常,成员变量被限制为 private 或 protected,成员函数被设置为 public。

三、 实例化概念类:

类(Class)

类是一种用户自定义的数据类型,它描述了一组具有相同属性(成员变量)和行为(方法)的对象。类就像是一个蓝图或模板,规定了基于这个类创建的所有对象应该具有的特征和行为。但是,类本身并不占用内存中的实际空间,它只是一个模板或者说是对象的定义

对象(Object)

对象是类的一个实例。当我们使用类创建一个对象时,实际上是在内存中为该对象分配了一块空间来存储它的状态(即成员变量的值)。每个对象都有自己独立的一套成员变量副本,这意味着即使是从同一个类创建出来的不同对象,它们之间也不会相互影响。

实例化(Instantiation)

定义:实例化是根据类的定义创建具体对象的过程。
过程:
内存分配:为对象分配内存空间。
初始化:设置对象的初始状态(即成员变量的初始值)。

#include <iostream>
using namespace std;

class Person {
private:
    string name;
    int age;

public:
    // 构造函数
    Person(string n, int a) : name(n), age(a) {}

    // 方法
    void introduce() const {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

int main() {
    // 实例化一个Person对象
    Person person1("Alice", 30);

    // 调用对象的方法
    person1.introduce();

    return 0;
}

总结
类:定义了对象的结构和行为。
对象:类的具体实例,具有独立的状态和行为。
实例化:根据类的定义创建对象的过程。

四、 对象大小

成员变量的大小:每个成员变量都有自己的大小,例如 int 通常占 4 字节,char 占 1 字节,double 占 8 字节等
对齐规则:为了提高内存访问效率,编译器会对成员变量进行对齐处理。这意味着成员变量可能不会紧挨着存储,而是会有一定的填充字节。
虚函数表指针:如果类中有虚函数,那么每个对象会包含一个指向虚函数表的指针,这个指针通常占用 4 或 8 字节(取决于平台)。

#include <iostream>

class MyClass {
public:
    char a;      // 1 byte
    int b;       // 4 bytes
    double c;    // 8 bytes
};

int main() {
    std::cout << "Size of MyClass: " << sizeof(MyClass) << " bytes" << std::endl;
    return 0;
}

成员变量的大小:
char a 占 1 字节
int b 占 4 字节
double c 占 8 字节

对齐规则:
编译器通常会对成员变量进行对齐处理,以确保每个成员变量的地址是其大小的倍数。例如,int 必须对齐到 4 字节边界,double 必须对齐到 8 字节边界。
假设 MyClass 对象的布局如下:
char a 占 1 字节
填充 3 字节(使 int b 对齐到 4 字节边界)
int b 占 4 字节
double c 占 8 字节
因此,MyClass 对象的总大小为:
1 + 3 + 4 + 8 = 16 字节

如果类中包含虚函数,那么每个对象还会包含一个指向虚函数表的指针(vptr)。

#include <iostream>

class MyVirtualClass {
public:
    virtual void foo() {}
    char a;      // 1 byte
    int b;       // 4 bytes
    double c;    // 8 bytes
};

int main() {
    std::cout << "Size of MyVirtualClass: " << sizeof(MyVirtualClass) << " bytes" << std::endl;
    return 0;
}

在这种情况下,MyVirtualClass 对象的布局可能会是:

vptr 占 8 字节(假设 64 位系统)
char a 占 1 字节
填充 3 字节(使 int b 对齐到 4 字节边界)
int b 占 4 字节
double c 占 8 字节

因此,MyVirtualClass 对象的总大小为:

8 + 1 + 3 + 4 + 8 = 24 字节

成员变量的大小 是决定对象大小的主要因素。
对齐规则 可能会导致额外的填充字节。
虚函数 会增加一个指向虚函数表的指针,从而增加对象的大小。

五、this 指针的基本概念

this 指针的作用:

this 指针是一个指向当前对象的指针,它在每个非静态成员函数中都是隐式存在的。
当一个对象调用其成员函数时,this 指针会被自动传递给该成员函数,使得成员函数能够访问和操作调用该函数的对象的成员变量和成员函数。

this 指针的类型:

this 指针的类型是当前类的指针类型。例如,对于 Date 类,this 指针的类型是 Date*。

this 指针的隐式传递:

在成员函数的参数列表中,this 指针是隐式的,编译器会自动处理。你不需要在调用成员函数时显式传递 this 指针

#include <iostream>

class Date {
public:
    // 构造函数
    Date() : _year(0), _month(0), _day(0) {}

    // 初始化函数
    void Init(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }

    // 打印函数
    void Print() const {
        std::cout << _year << "-" << _month << "-" << _day << std::endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1, d2;

    // 调用Init函数初始化d1
    d1.Init(2023, 10, 5);

    // 调用Init函数初始化d2
    d2.Init(2024, 11, 7);

    // 调用Print函数打印d1
    d1.Print(); // 输出: 2023-10-5

    // 调用Print函数打印d2
    d2.Print(); // 输出: 2024-11-7

    return 0;
}

4 、C++和C语言实现Stack对比

在C语言和C++语言中实现栈(Stack)时,虽然它们的底层逻辑相似,都是遵循后进先出的原则,但是由于两种语言特性的不同,实现的方式和风格上存在显著差异。

在C语言中,通常使用结构体(struct)来定义栈的数据结构,并通过一组函数来操作这个结构体。例如,定义一个简单的整数栈可能需要如下步骤:

定义一个结构体来表示栈,包括一个数组或指针用于存储元素,以及一个变量记录栈顶的位置。
编写一系列函数(如push, pop, isEmpty等),这些函数接受指向栈结构体的指针作为参数,以执行相应的操作。
在这种实现中,数据和操作数据的方法是分离的,客户端代码可以直接访问结构体中的字段。这意味着没有访问控制,任何人都可以随意修改结构体内部的状态,这可能会导致程序不稳定。

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
 STDataType* a;
 int top;
 int capacity;
}ST;
void STInit(ST* ps)
{
 assert(ps);
 ps->a = NULL;
 ps->top = 0;
 ps->capacity = 0;
}
void STDestroy(ST* ps)
{
 assert(ps);
 free(ps->a);
 ps->a = NULL;
 ps->top = ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)
{
 assert(ps);
if (ps->top == ps->capacity)
 {
 int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
 STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * 
sizeof(STDataType));
 if (tmp == NULL)
 {
 perror("realloc fail");
 return;
 }
 ps->a = tmp;
 ps->capacity = newcapacity;
 }
 ps->a[ps->top] = x;
 ps->top++;
}
bool STEmpty(ST* ps)
{
 assert(ps);
 return ps->top == 0;
}
void STPop(ST* ps)
{
 assert(ps);
 assert(!STEmpty(ps));
 ps->top--;
}
STDataType STTop(ST* ps)
{
 assert(ps);
 assert(!STEmpty(ps));
 return ps->a[ps->top - 1];
}
int STSize(ST* ps)
{
 assert(ps);
return ps->top;
}
int main()
{
 ST s;
 STInit(&s);
 STPush(&s, 1);
 STPush(&s, 2);
 STPush(&s, 3);
 STPush(&s, 4);
 while (!STEmpty(&s))
 {
 printf("%d\n", STTop(&s));
 STPop(&s);
 }
 STDestroy(&s);
 return 0;
}

C++语言实现
而在C++中,可以通过定义一个类(class)来实现栈,这种方式利用了封装这一面向对象编程的核心概念:

类中包含了私有成员(private members),用于存储栈的数据和状态,外部无法直接访问这些成员。
公共成员函数(public member functions)提供了对外部访问的接口,如push, pop, isEmpty等方法,这些方法内部实现了对私有数据的操作。
构造函数可以初始化栈,而析构函数可以清理资源,确保栈在生命周期结束时正确释放内存。
通过这种方式,C++的实现不仅隐藏了数据的具体实现细节,还保护了数据不被外部代码随意修改,从而提高了代码的安全性和可维护性。此外,C++中的成员函数自动接收一个this指针,该指针指向调用该函数的对象实例,这使得成员函数的调用更加简洁方便。

默认参数:C++支持函数参数的默认值,这在某些情况下可以简化函数调用,提高代码的灵活性。
类型别名:虽然C++也支持typedef关键字,但更推荐使用using关键字来创建类型别名,这在C语言中是不可用的。
模板:C++的模板功能允许创建泛型栈,即可以在编译时指定栈中存储的元素类型,而无需为每种类型编写单独的栈实现
 

#include <iostream>
#include <stdexcept>

class Stack {
private:
    static const int MAX_SIZE = 100;
    int data[MAX_SIZE];
    int top;

public:
    Stack() : top(-1) {}

    bool is_empty() const {
        return top == -1;
    }

    void push(int value) {
        if (top >= MAX_SIZE - 1) {
            throw std::overflow_error("Stack Overflow");
        }
        data[++top] = value;
    }

    int pop() {
        if (is_empty()) {
            throw std::underflow_error("Stack Underflow");
        }
        return data[top--];
    }
};

int main() {
    Stack s;

    s.push(1);
    s.push(2);
    s.push(3);

    while (!s.is_empty()) {
        std::cout << s.pop() << " ";
    }

    return 0;
}

;