【c++】类和对象:让你明白“面向一个对象有多重要”:构造函数,析构函数,拷贝构造函数的深入学习(上)

简介: 【c++】类和对象:让你明白“面向一个对象有多重要”:构造函数,析构函数,拷贝构造函数的深入学习(上)

什么是面向对象?



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


比如洗衣服:

7e24e3d1122544df93b2155af8b31cc6.png


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


f8786edf1e2547dc9e55ff0c4b2ea865.png


在C语言中很多的过程在c++中被分为了人 衣服 洗衣机 洗衣粉,想要完成洗衣粉这件事只需要人将衣服放进洗衣机,倒入洗衣粉,启动洗衣机就完成了。


一、类是什么?



C语言结构体中只能定义变量,在c++中结构体内不仅可以定义变量,也可以定义函数。比如:之前我们用C语言方式实现的栈,结构体中只能定义变量,现在以c++的方式实现,会发现struct中也可以定义函数。


struct Stack
{
  void Init(int n = 4)
  {
       a = (int*)malloc(sizeof(int) * n);
  if (nullptr == a)
  {
    perror("malloc申请空间失败");
    return;
  }
  capcity = n;
  top = 0;
  }
  void Push(int x)
  {
  }
  void Pop()
  {
  }
  int Top()
  {
  }
  bool Empty()
  {
  }
  int* a;
  int capcity;
  int top;
};
int main()
{
  Stack st;
  st.Init();
  st.Push(1);
  st.Push(2);
  st.Push(3);
  return 0;
}

就像上面的代码段一样,以前C语言是不支持将函数写入结构体的,而c++现在能做到了。


而上面的struct在c++中更喜欢用class来代替。


那么怎么创建一个类呢?看下面代码段:


class classname    // class后面跟你自己想要取的类名
{
  //类体,由成员函数和成员变量组成
};  //后面一定要加;和结构体一样


class为定义类的关键字,classname为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中的内容称为类的成员,类中的变量称为类的属性或成员变量,类中的函数称为类的方法或者成员函数。类中定义的变量都可以直接在类中使用,类外则需要域限定符。


类的两种定义方式:


1.声明和定义全部放在类体中,需要注意的是,成员函数如果在类中定义,编译器可能会当成内联函数来处理。比如下面这样的:


class classname
{
public:
  void add()
  {
  year++;
  }
private :
  int year;
};


2.类声明放在头文件中,成员函数的定义放在.cpp文件中。


class classname
{
public:
  void add();
private :
  int year;
};
void classname::add()
{
  year++;
}


类的访问限定符:


c++中有三种访问限定符,分为public(公有),private(私有)私有的在类外不可以访问,protected(受保护的)同样在类外不可以访问。


c++实现封装的方式:用类将对象的属性和方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。


1.pubic修饰的成员在类外可以直接被访问

2.protected和private修饰的成员在类外不能直接被访问

3.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。

4.如果后面没有访问限定符,作用域到  }  及类结束。

5.class的默认访问权限为private,struct的默认访问权限为public(因为要兼容C语言)


注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。


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;
  d1.Init(2023,2,5);
  d1.Print();
  return 0;
}


大家觉得上面这个代码段可以成功打印出年月日吗?答案是不可以,因为我们在private中定义的年与Init函数传来的参数year一样,这就导致编译器识别不出来,所以我们在定义成员变量的时候最好都像month那样在前面加个符号用来区分。

32e7e5f34a8a49a484ed193b2db866d9.png


封装 :


面向对象的三大特性:封装,继承,多态。


封装的意思就是将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装的本质就是一种管理,让用户更方便的使用类。举个例子:就像电脑的主机一样,主机只提供开机键等接口,而实际上电脑真正工作的东西是cpu等硬件,而这些硬件是不会暴露在外边让用户看到的。


类的实例化:


用类的类型创建对象的过程,称为类的实例化。类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它,比如以下代码:

9142a051f33b488ea5d30fdb9622338b.png

3bfc7a22044244c68ce520b7306edb8b.png


我们已经说过类是对对象进行描述的,只有创建了一个类对象,才会给这个类对象分配空间,这样就可以使用类里面的函数等变量。下图为正确的使用方式:


66e88c3feae44dfcb143875fad6d6a5c.png


下面我们来看类中成员如何存储的:


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;
  cout << sizeof(d1) << endl;
  return 0;
}


大家可以猜一猜d1这个对象的大小是多少?答案是12,12不就是private中三个成员变量的大小吗,为什么成员函数不占用空间呢?


90d90eb9c48e47fba9ba910838ab7f05.png


大家看上图,d1的year变量和d2的year变量是在同一块空间吗?答案是不在同一块空间,因为对象实例化后会给每个对象都开辟一个空间,那么d1的year肯定是在d1这个对象开辟的空间内,d2的year是在d2这个对象开辟的空间内。

e3949817191144409ee4326ab28b5fee.png

那么 d1的init函数和d2的init函数是在同一块空间吗?答案是是的,c++中为了防止每个对象都开辟空间存储不同的函数所以将函数放在了公共的代码段,想要调用这个函数直接去公共的代码段去找即可,这也就解释了为什么我们再计算对象的大小的时候不包含函数的大小了。至于为什么成员变量不设为公共的问题就很好回答了,应该每个对象都能对自己的成员变量进行修改,如果设为一个公共的那么d2对象修改year的值也会将d1对象的year进行修改。


// 类中既有成员变量,又有成员函数
class A1 {
public:
  void f1() {}
private:
  int _a;
};
// 类中仅有成员函数
class A2 {
public:
  void f2() {}
};
// 类中什么都没有---空类
class A3
{};


上面这三个类的sizeof大小是多少呢?


有了上面的解释回答这道题就很容易了,首先A1中只有变量_a占实际空间,所以大小为4字节。


A2中只有成员函数,而成员函数在代码段中那么这个A2就相当于A3是一个空类,空类在c++中占一个字节。


this指针


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;
  Date d2;
  d1.Init(1, 2, 3);
  d2.Init(4, 5, 6);
  d1.Print();
  d2.Print();
  return 0;
}

对于上面的代码段,有这样一个问题:Date类中有Init和Print两个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init函数时,该函数是如何知道设置d1对象,而不是设置d2对象呢?


对于这个问题,c++中引用了this指针来解决这个问题。即:c++编译器给每个"非静态的成员函数"增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有"成员变量"的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需用来传递,编译器自动完成。比如下面代码:


class Date
{
public:
  void Init(int year, int month, int day)//用户看到的
  //实际上  void Init(Date* this,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;
  Date d2;
  d1.Init(1, 2, 3);//用户看到的
  //实际上
  //d1.Init(&d1, 1, 2, 3);
  d2.Init(4, 5, 6);
  d1.Print();
  d2.Print();
  return 0;
}


在这里要注意,我们不能显式的自己去调用的时候传入对象的地址,这样编译器会报错。


那么this指针有什么作用呢?


class Date
{
public:
  void Init(int year, int month, int day)
  {
  this->year = year;
  this->month = month;
  this->day = day;
  cout << this << endl;
  }
  void Print()
  {
  cout << year << "年" << month << "月" << day << "日" << endl;
  }
private:
  int year;   ///这些只是声明并不是定义
  int month;
  int day;
};
int main()
{
  Date d1;
  Date d2;
  d1.Init(1, 2, 3);
  d2.Init(4, 5, 6);
  d1.Print();
  d2.Print();
  return 0;
}

a07e4f26d5b245c49e78128fc9de4db0.png


之前Init函数中不能分辨的year等变量用上this指针就可以正确分辨,还可以打印此对象的地址


那么this指针存放在哪里呢?this指针存放在栈中,因为this是隐含形参/vs下面是存在ecx寄存器中


this指针可以为空吗?看以下代码:


class Date
{
public:
  void Init(int year, int month, int day)
  {
  this->year = year;
  this->month = month;
  this->day = day;
  cout << this << endl;
  }
  void Print()
  {
  cout << year << "年" << month << "月" << day << "日" << endl;
  }
  void Func()
  {
  cout << "Func()" << endl;
  }
private:
  int year;   ///这些只是声明并不是定义
  int month;
  int day;
};
int main()
{
  Date* ptr = nullptr;
  ptr->Func();
  return 0;
}


上面这段代码可以正常编译吗?很多人看到ptr是个空指针然后去调用Func函数会觉得这里对空指针进行解引用了,这样理解其实是不对的,首先这个代码可以正常编译看下图:


aa0e9e7973a149db8b6bf1f3ae7451d8.png


39ef39a4e26d4b1090b22333d8db7f9e.png



这里解释一下为什么可以编译,我们之前说过调用类中的函数时编译器会隐式修改为传对象的地址然后函数多了一个this指针的参数,所以当我们调用func这个函数的时候,编译器通过this指针找到了类中的这个函数即使把ptr这个空指针传给了this,也是可以正常使用的。那么下面这个程序的运行结果又是怎么样的?


af4a0aa973324b2e94ef0d978b09b93a.png

9e74f41a112d46a9847785e5c681631e.png

 

上图这段代码运行起来程序崩溃了,首先这个Init和刚刚的func函数一样都不在对象里面,他们都在公共区域,调用这个函数直接跳到存放代码的地址,这些都没问题,有问题的是ptr是空指针,ptr给this传了个空指针然后再Init函数中这个空指针指向Year这个变量,这就是对空指针进行解引用了。


f9d09b2483414813a2ebcee3d10c7b8f.png


那么上图中这个代码是否可以正常运行呢?很多人看到括号内对ptr空指针进行解引用了以为程序会崩溃,但其实并不是,编译器还是先去对象里找有没有Func()这个函数,然后编译器发现找不到通过this指针找到了Func函数的公共代码段,而这里的(ptr)是起到了给传给this指针的作用。


e7eef19f0d6a49259667a7da6acb4200.png


那么上图中的这个代码运行起来会不会崩溃呢? 这个一定是崩溃了,编译器先去找year是不是在对象里,找到后发现这个对象有自己的空间所以对空指针进行解引用了。通过上面几个问题大家应该知道this指针是可以为空的了。


目录
相关文章
|
1天前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
1天前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
1天前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
1月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
8天前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
37 16
|
12天前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
55 6
|
1月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
1月前
|
安全 编译器 C语言
【C++篇】深度解析类与对象(中)
在上一篇博客中,我们学习了C++类与对象的基础内容。这一次,我们将深入探讨C++类的关键特性,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载、以及取地址运算符的重载。这些内容是理解面向对象编程的关键,也帮助我们更好地掌握C++内存管理的细节和编码的高级技巧。
|
1月前
|
存储 程序员 C语言
【C++篇】深度解析类与对象(上)
在C++中,类和对象是面向对象编程的基础组成部分。通过类,程序员可以对现实世界的实体进行模拟和抽象。类的基本概念包括成员变量、成员函数、访问控制等。本篇博客将介绍C++类与对象的基础知识,为后续学习打下良好的基础。
|
2月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
86 19

热门文章

最新文章