C++类和对象(上)

目录

[一. 类的定义](#一. 类的定义)

[1. 类定义的格式](#1. 类定义的格式)

[2. 访问限定符](#2. 访问限定符)

[3. 类域](#3. 类域)

[二. 实例化](#二. 实例化)

[1. 实例化概念](#1. 实例化概念)

[2. 对象大小](#2. 对象大小)

[三. this指针](#三. this指针)

[1. this指针概念](#1. this指针概念)

[2. 针对this指针的三道练习题:](#2. 针对this指针的三道练习题:)

[四. C++和C语言实现Stack (栈) 的对比](#四. C++和C语言实现Stack (栈) 的对比)


一. 类的定义

1. 类定义的格式

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

cpp 复制代码
#include<iostream>
using namespace std;
class Stack
{
private:
	void Push(int x)  //定义类的函数
	{}
	void Pop()
	{}
	int Top()
	{}

public:
	int* a;  //定义类的变量
	int top;
	int capacity;
};

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


为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前⾯或者后⾯加 "_" 或者 "m" 开头,注意C++中这个并不是强制的,只是⼀些惯例,具体看公司的要求。

为什么要加特殊标识符,比如图下情况:

cpp 复制代码
#include<iostream>
using namespace std;
class Student 
{
private:
    int age; //成员变量

public:
    void Age(int age) //形参名和成员变量重名了
    { 
        age = age; //这里只会给形参赋值,成员变量根本没改
    }
};

在 C++ 类里,成员变量和成员函数的参数、局部变量很容易重名。而在下图更改后可避免错误传参这类问题:

cpp 复制代码
#include<iostream>
using namespace std;
class Student 
{
private:
    int _age; //加m_前缀,一眼识别是成员变量

public:
    //形参只能用普通命名,和成员变量区分
    void Age(int age) 
    {
        _age = age; //无歧义,不会出错
    }
};

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

cpp 复制代码
typedef struct LinkListC { //c++兼容c语言的struct用法
    int val;
    struct LinkListC* next;
};

struct LinkListCPP {
    void Init()  //可以定义函数
    {
        next = nullptr;
        val = x;
    }
    int val;
    LinkListCPP* next;
};

c语言struct结构体中只可以定义类的变量,而c++中class定义不光可以定义类的变量,还可以定义类的函数,这是c++更便利的一点。


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

C++ 标准明确规定:在类的定义体内部直接实现的成员函数,自动成为 inline 函数而在类的定义题外部实现的成员函数 (定义与声明分离),默认不是 inline 函数,如果想变成内联函数,需要手动添加 inline 定义。

cpp 复制代码
#include<iostream>
using namespace std;
class Student 
{
private:
    int m_age = 18;

public:
    //1. 类内直接实现:默认inline,无需手动加
    int Age1() {
        return m_age;
    }

    //仅声明,类外实现:默认不是inline
    void Age2(int age);
};

//类外实现,默认不是inline
void Student::Age2(int age) 
{
    m_age = age;
}

//如果要让类外实现的函数变成inline,必须手动加inline
inline void Student::Age2(int age) 
{
    m_age = age;
}

2. 访问限定符

c++访问限定符分为:public(公开)private(私有)protected(受保护) 三种,是封装思想的核心。public 修饰的成员在类外可以直接被访问;protectedprivate修饰的成员在类外不能直接被访问,protected和private是⼀样的,在继承的场景下才能体现出他们的区别,以后再说,现在俺也不会。
访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现 时为⽌,如果后⾯没有访问限定符,作⽤域就到**}**即类结束。

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }   //public限制到这里(private之前)结束
private:
    int _year; 
    int _month;
    int _day;
};      //private限制到 } 这里结束

int main()
{
    Date d;
    d.Init(2024, 3, 31);
    return 0;
}

class定义成员没有被访问限定符修饰时默认为private ,struct默认为public 。⼀般成员变量都会被限制为private/protected ,需要给别⼈使⽤的成员函数会放为public

cpp 复制代码
#include<iostream>
using namespace std;
class CClass //class: 无手动添加限定符时,默认private
{
    int x; 
    void func() 
    {} 
};

struct SStruct  //struct: 无手动添加限定符时,默认public
{
    int x; 
    void func() 
    {} 
};

int main() 
{
    CClass c;
    //class默认private,类外不能访问
    //c.x = 10;
    //c.func();

    SStruct s;
    //struct默认public,类外可以直接访问
    s.x = 20;
    s.func();
    cout << s.x << endl;

    return 0;
}

3. 类域

C++包含局部域、全局域、命名空间域和类域四个域。其中类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤:: 作用域操作符指明成员属于哪个类域 (eg:这个函数是 Stack 类的成员,去 Stack 的类域里找它的变量)。

cpp 复制代码
#include<iostream>
using namespace std;
class Stack //定义一个类Stack,创建了独立的类域
{
private:
    int array[10]; //类内成员,属于Stack的类域
    int top;
public:
    void Init(); //仅在类内声明,函数体在类外定义
};

void Stack::Init() //类外定义成员:必须加 类名:: ,指明属于Stack类域
{
    top = 0; //编译器知道这是Stack类的成员,能正常找到
    for (int i = 0; i < 10; i++) 
    {
        array[i] = 0;
    }
}

int main() 
{
    Stack s;
    s.Init(); //调用成员函数
    cout << "栈初始化完成" << endl;

    return 0;
}

若类外定义成员函数时不写 类名 :: ,编译器会将其当作全局函数,无法找到类内的array等成员,直接报错;加上 类名 :: 后,编译器会识别这是类的成员函数,自动到对应类域中查找成员,编译正常通过。

cpp 复制代码
#include<iostream>
using namespace std;
class Stack 
{
private:
    int array[10];
    int top;
public:
    void Init();
};

//void Init() //没加Stack::,编译器把Init当成全局函数,会导致top和array报错 
{
//    top = 0; // 编译器:全局域里找不到top这个变量,报错
//    for (int i = 0; i < 10; i++) {
//        array[i] = 0; // 编译器:全局域里也找不到array,报错
//    }
//}

int main() 
{
    Stack s;
    s.Init();

    return 0;
}

二. 实例化

1. 实例化概念

  • **类实例化:**用类类型在内存中创建对象的过程。
  • 类的本质: 是对象的抽象描述(类似建筑设计图),仅声明成员变量,不分配内存,无法存储数据。
  • 对象的本质: 类实例化的产物,分配实际物理内存,用于存储成员变量;一个类可以实例化出多个独立对象。

类的本质

cpp 复制代码
#include<iostream>
using namespace std;
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;
};

类就相当于建筑设计图Date类只是一张设计图,它只规定了:所有日期对象都要有_year/_month/_day三个成员,还有Init和Print两个操作函数,这些成员及函数仅是声明,并未开空间,只有在进行类的实例化的时候才会开辟空间。


类实例化:

cpp 复制代码
int main()
{
    //Date类实例化出对象d1和d2
    Date d1;
    Date d2;

相当于Date是我们写好的一个模板d1d2是用这个模板做出来的实体

执行++Date d1;++ 这行代码时,编译器会在内存里给 d1 开辟一块空间 ,按照Date类的结构,创建出一个实实在在的Date对象;同理++Date d2;++就是再创建一个独立的对象。


对象的本质

cpp 复制代码
    d1.Init(2024, 3, 31);
    d1.Print();

    d2.Init(2024, 7, 5);
    d2.Print();

    return 0;
}

对象是类实例化出来的实体,会占实际内存,用来存储类成员的变量d1d2Date类实例化出来的实体,编译器给它们各自分配了独立的物理内存 ,用来存_year/_month/_day的值。

一个类可以造多个独立对象d1d2都是Date类做出来的,但它们是两个完全独立的对象,各有各的内存:d1存的是2024/3/31d2存的是2024/7/5,修改d1的数据不会影响d2,就像用同一张设计图盖两栋房子,两栋房子互不干扰


2. 对象大小

C++ 类实例化的对象中,只存储成员变量,不存储成员函数

  • **成员变量:**每个对象有独立副本,存储各自数据,占用对象内存。
  • **成员函数:**所有对象共享同一份代码,存放在代码段,不占用对象内存,避免冗余浪费。
  • 补充:对象内存需遵循内存对齐规则。
cpp 复制代码
#include<iostream>
using namespace std;
class Date 
{
private:
    //成员变量:每个对象独立存储
    int _year;
    int _month;
    int _day;
public:
    //成员函数:所有对象共享同一份代码
    void Init(int y, int m, int d) 
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Print() 
    {
        cout << _year << "/" << _month << "/" << _day << endl;
    }
};

int main() 
{
    Date d1, d2; //实例化两个对象
    d1.Init(2024, 3, 31);
    d2.Init(2024, 7, 5);

    //验证1:对象大小只和成员变量相关,和函数无关
    cout << "sizeof(d1) = " << sizeof(d1) << endl; //输出12(3个int,3*4=12)
    cout << "sizeof(d2) = " << sizeof(d2) << endl; //输出12,和d1一致

    //验证2:成员函数地址相同,证明共享
    cout << "d1.Init地址: " << &Date::Init << endl;
    cout << "d2.Init地址: " << &Date::Init << endl; //和d1的地址完全一致

    //验证3:成员变量独立,数据互不影响
    d1.Print(); //2024/3/31
    d2.Print(); //2024/7/5

    return 0;
}

内存对齐规则:

  • 第⼀个成员在与结构体偏移量为 0 的地址处。
  • 其他成员变量要对齐到某个数字 (对齐数) 的整数倍的地址处。
  • 注意:对齐数=编译器默认的⼀个对齐数 与该成员大小的较小值
  • VS中默认的对齐数为8
  • 结构体总大小为:最⼤对齐数 (所有变量类型最大者与默认对齐参数取最小) 的整数倍
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数 (含嵌套结构体的对齐数) 的整数倍。

为什么要进行内存对齐:

内存对齐是 CPU 为了读内存更快、不崩溃,和编译器约定的 "内存摆放规则",是典型的用空间换时间的设计。

cpu并非一个字节一个字节读取内存,对于32位cpu来说是一次读4个字节、对于64位cpu来说一次读8个字节,所以为了cpu将内存分成了以4字节 (仅32位) 、8字节 (仅64位) 为大小的内存块,每次直接读一个块的内容,这样效率最高。

借用下面A的实例化对象大小来说明如果不进行内存对齐的后果:

如果不进行内存对齐,在char于0位置存储后,int紧接着于1位置存储 (1~4)。cpu开始读取内存,读char进行一次,然后从1开始找int到3结束进行一次,但是这时候int并未读取完整,所以cpu还会继续往后找,从4开始找到了剩下的一点算是进行读取一次,总共需要读取三次;

而进行内存对齐的话 (0字节存放char,1~3字节空白填充用于对齐,4~7存放int) ,cpu从0位置开始一次性读4个字节,读取到一个char,然后直接进行下一个块的查找,读取到一个int,一共只需要进行两次操作,在需读取量大的时候对齐于不对齐的差距会变得非常大。


分别计算A、B、C的实例化的对象有多大?

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
    void Print()
    {
        cout << _ch << endl;
    }
private:
    char _ch;
    int _i;
};

所以sizeof(a) = 8;

cpp 复制代码
#include<iostream>
using namespace std;
class B
{
public:
    void Print()
    {
        //...
    }
};

class C
{
};

C++标准规定:空类实例化的对象大小为**1字节** (用于标识对象的内存地址,区分不同对象)

对于b、c来说,无任何成员变量,属于**空类。**所以sizeof(b) = 1、sizeof(c) = 1,这⾥给1字节,纯粹是为了占位标识对象存在。


三. this指针

1. this指针概念

this 指针就是编译器偷偷给成员函数加的一个 "隐藏参数",它存着当前调用这个函数的对象的地址,让函数知道自己是在操作 d1 还是 d2,我们不用手动传参,但可以在函数里用它访问对象的成员。

图中**//部分** 是带上隐藏this指针的代码写法 (C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。) :

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
    //void Init(Date* const this, int year, int month, int day)
    void Init(int year, int month, int day)
    {
        //this->_year = year;
        //this->_month = month;
        //this->_day = day;
        _year = year;
        _month = month;
        _day = day;
    }
    //void Print(Date* const this)
    void Print()
    {
        //函数体内显示使用this指针
        cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
        cout << _year << "/" << _month << "/" << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1;
    Date d2;

    //d1.Init(&d1, 2024, 3, 31);
    d1.Init(2024, 3, 31);
    //d1.Print(&d1);
    d1.Print();

    //d2.Init(&d2, 2024, 7, 5);
    d2.Init(2024, 7, 5);
    //d2.Print(&d2);
    d2.Print();

    return 0;
}

编译器自动加的隐藏形参:

编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this 指针。比如Init函数的真实写法为:void Init(Date* const this, int year, int month, int day)

const 的意思是:this 指针本身不能被修改 (不能让它指向别的对象),但 this 指向的对象的成员变量是可以修改的。


成员函数怎么区分不同对象?

在主函数内this指针通过形参的一号位附带取不同对象的地址 来区别是由哪个对象来访问成员函数:比如d1.Init(2024, 3, 31); 的真实写法为d1.Init(&d1, 2024, 3, 31); 同理对于d2对象的也是d2.Init(&d2, 2024, 7, 5);


成员变量的访问本质 + this 指针的使用规则:

类的成员函数中访问成员变量,本质是通过this指针来访问,比如*_year = year;* 的真实写法为this->_year = year;


2. 针对this指针的三道练习题:

cpp 复制代码
1. this指针存在内存哪个区域的 ()
A.栈 B.堆 C.静态区 D.常量区 E.对象⾥⾯

答案:A

本题考查两个知识点:

this 指针的本质 :this 指针是编译器自动给非静态成员函数 添加的隐藏形参,本质就是一个普通的函数形参变量。

函数形参的存储位置 :所有函数的 (包括this指针) 形参、局部变量,都存储在栈内存中(函数调用时在栈上开辟空间,函数执行结束自动释放)。

常见误区纠正 :this 指针不存储在对象内部,对象里只存成员变量,成员函数和 this 指针都不占对象的内存。

堆:堆内存是手动malloc申请的空间,this指针是编译器自动管理的形参,不会存在堆里。

静态区:静态区存全局变量、静态变量,this是临时的形参,不属于全局/静态变量。

常量区:常量区存字符串常量、const修饰的常量,this是指针变量,不是常量。

对象里面:对象里只存成员变量,this指针是函数的形参,不存储在对象中。

结论:this 指针是成员函数的隐藏形参,本质是普通的函数参数,因此存储在栈内存中,不存储在对象内部,也不存在堆、静态区、常量区。


cpp 复制代码
2.下⾯程序编译运⾏结果是 ()
A、编译报错 B、运⾏崩溃 C、正常运⾏
#include<iostream>
using namespace std;

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

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

    return 0;
}

答案:C

本题考查两个知识点:

①成员函数不存储在对象中:所有对象共享同一份成员函数代码,代码存在代码段,对象里只存成员变量。

②成员函数靠 this 指针区分对象:编译器会自动给成员函数加一个隐藏的this形参,调用时把对象的地址传给this。

对于步骤p->Print();

①编译器看到 p->Print(); ,会自动把它转换成全局函数调用 的形式:A::Print(p);

②而Print函数的真实原型,编译器会自动加上隐藏的this指针:void Print(A* const this) { cout << "A::Print()" << endl; }

③所以最终执行的代码是:*A::Print(nullptr);*也就是把空指针p的值,传给了隐藏的this指针。

空指针并不会报编译错误,可正常运行,只有在解引用空指针时才会崩溃。

结论:空指针调用成员函数不一定会崩溃,结果完全取决于函数内部是否访问了成员变量:

函数不访问任何成员变量------正常运行;函数访问成员变量------运行崩溃 (解引用空指针)


cpp 复制代码
3.下⾯程序编译运⾏结果是 ()
A、编译报错 B、运⾏崩溃 C、正常运⾏
#include<iostream>
using namespace std;

class A
{
public:
    void Print()
    {
        cout << "A::Print()" << endl;
        cout << _a << endl;
    }
private:
    int _a;
};
int main()
{
    A* p = nullptr;
    p->Print();

    return 0;
}

答案:B

本题涉及知识点与上一题相同。本题解析只说不同部分:

在执行代码 A::Print(nullptr); 时,第一步执行cout << "A::Print()" << endl; 此时不会有任何问题,仅仅是打印一个字符串,没有用到this指针去解引用啥的,所以可以正常输出;但当执行到第二步cout << _a << endl; 时,该代码等同于 cout << this->_a << endl; ,而此时this指针的值是nullptr,此时尝试使用空指针去访问成员变量_a相当于解引用空指针,直接运行崩溃。

结论:空指针调用成员函数不一定会崩溃,结果完全取决于函数内部是否访问了成员变量:

函数不访问任何成员变量------正常运行;函数访问成员变量------运行崩溃 (解引用空指针)


四. C++和C语言实现Stack (栈) 的对比

C++与C语言实现栈 (Stack) 的底层逻辑一致,核心差异体现在面向对象的封装特性 与语法便利性上,是C++面向对象三大特性 (封装、继承、多态) 中封装的直观体现。
封装特性

C++将栈的数据和操作函数统一封装到类中,通过private等访问限定符限制数据的直接访问与修改,避免随意篡改数据,实现更严格、规范的代码管理,提升安全性。

语法便利性优化

成员函数无需手动传递对象地址,由this指针隐式自动传递,简化调用;

支持缺省参数等便捷语法,无需typedef,直接用类名操作,代码更简洁。

本质说明

入门阶段用类实现的栈,仅在代码组织、安全性、语法上做了优化,底层数据结构与逻辑和 C语言实现完全一致;后续 STL 中的栈会进一步体现C++的设计优势。

C实现Stack代码:

cpp 复制代码
#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++实现Stack代码:

cpp 复制代码
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
    //成员函数
    void Init(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc申请空间失败");
                return;
        }
        _capacity = n;
        _top = 0;
    }
        void Push(STDataType x)
        {
            if (_top == _capacity)
            {
                int newcapacity = _capacity * 2;
                STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
                    sizeof(STDataType));
                if (tmp == NULL)
                {
                    perror("realloc fail");
                    return;
                }
                _a = tmp;
                _capacity = newcapacity;
            }
            _a[_top++] = x;
        }
        void Pop()
        {
            assert(_top > 0);
            --_top;
        }
        bool Empty()
        {
            return _top == 0;
        }
        int Top()
        {
            assert(_top > 0);
            return _a[_top - 1];
        }
        void Destroy()
        {
            free(_a);
            _a = nullptr;
            _top = _capacity = 0;
        }
private:
    //成员变量
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};
int main()
{
    Stack s;
    s.Init();
    s.Push(1);
    s.Push(2);
    s.Push(3);
    s.Push(4);
    while (!s.Empty())
    {
        printf("%d\n", s.Top());
        s.Pop();
    }
    s.Destroy();
    return 0;
}
相关推荐
️是782 小时前
信息奥赛一本通(4005:【GESP2306一级】时间规划)
数据结构·c++·算法
tankeven2 小时前
HJ174 交换到最大
c++·算法
xyq20242 小时前
SQL CREATE INDEX
开发语言
Дерек的学习记录2 小时前
Unreal Eangie 5:蓝图编程
开发语言·学习·ue5
hope_wisdom2 小时前
C/C++数据结构之树
数据结构·c++·二叉树·
添尹2 小时前
Go语言基础之指针
开发语言·后端·golang
2401_827499992 小时前
python项目实战10-网络机器人01
开发语言·python
哆啦阿梦2 小时前
Java AI 应用工程师 - 完整技能清单
java·开发语言·人工智能
磊 子2 小时前
八大排序之插入排序+希尔排序
数据结构·算法·排序算法