C++初阶--类和对象

1、对面向对象的初步认识

例如洗衣服的过程:

总共有四个对象:人、衣服、洗衣粉、洗衣机

整个过程主要是:人、衣服、洗衣粉、洗衣机 四个对象之间交互完成的,人类不需要关心洗衣机是如何洗衣服的,是如何甩干的。

2、类

2.1、类的两种定义方式

1、声明和定义全部都放在类体中,但是需要注意的是如果在类中定义,编译器可能会将其当成内联函数处理。

2、类声明在.h文件中,成员函数定义放在.cpp文件中。但是在.cpp中要在成员函数前面加 类名::

*一般情况下推荐第二种

2.2、类的访问限定符

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

C++中的struct与class有什么区别??

答案是: C++需要兼容C语言,所以C++中struct可以当成结构体使用。此外,C++中的struct也可以用来定义类。

和class定义类相同, 区别是:struct定义的类默认访问权限是public,class定义的类默认访问权限是private。

2.3、封装

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

那有一个问题:什么是封装????

是指将对象的状态(属性)和行为(方法)封装在一起,形成一个相对独立的、可复用的软件模块。封装可以有效的隐藏对象的实现细节,使对象能够被简化的使用,并提高代码的可复用性和安全性。

在封装中,代码的属性和方法都被视为对象内部细节,只有通过对象提供的公开接口才能访问和操作。因此,封装可以外部代码直接访问和修改对象的属性,从而减少了代码的耦合度,提高了代码的灵活性和可扩展性。

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行 交互。   封装本质上是一种管理,让用户更方便使用类

2.4、类的实例化

类是对对象进行描述的,是一个模型一样的东西。

定义的一个类并没有分配实际的内存空间来存储它。一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储成员变量。

错误示范:

void  Test()
{ 
  Person._age = 100; //Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄。
  Person::showinfo(); //错误
 //::  这个的用法是在 .cpp中定义成员函数的时候在函数名的前面加类名+::
  //正确写法:::
  Person man;
  man._age = 10;
  man.showinfo();
}

2.5、类对象的大小

空类和只有成员函数没有成员变量的类的大小都是1,空类的1 是为了占位。

结构体怎么对齐? 为什么要进行内存对齐?

如何让结构体咱找指定的对齐参数进行对齐?

什么是大小端?如何色是某台机器是打断还是小端,有没有遇到过考虑大小端的场景?

3、类成员的this指针

问题引入:如果说在一个类里面有两个成员函数,函数体中并没有关于不同对象的区分,此时,实例化出两个对象,d1、d2 当实例化出的对象d1调用其中一个函数的时候,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?

答案是:C++编译器给每个“非静态的成员函数”增加一个隐藏的this指针,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量、成员函数”的操作,都是通过该指针去访问

只不过所有的操作对用户都是透明的,即用户不需要传递,编译器自动完成。

3.1、this指针的特性

1、this指针的类型是 :   类类型*const    。在成员函数中,不能给this指针赋值。

2、只能在成员函数内部使用。

3、this指针本质上是成员函数的形参,当对象调用成员函数的时候,将对象的地址作为实参传递给this形参。所以对象不存储this指针。

4、this是成员函数第一个隐含的指针形参,一般情况下由编译器exc寄存器自动传递。

【补充问题】

1、this指针存在哪里?

当对象调用其成员函数时,系统会自动将该对象的地址以this指针的形式传递给该成员函数。

在成员函数内部,可以使用this指针来访问当前对象的成员变量和成员函数。因此,打印this指针可以打印当前对象的地址。

在C++中,this指针的使用使得成员函数能够清晰地区分出当前对象的成员变量和外部同名变量之间的区别。例如,在成员函数中,如果有一个与成员变量同名的局部变量,可以通过this指针来明确指示使用成员变量而不是局部变量。

总结:this指针存在于C++类的非静态成员函数中,用于指向当前对象,允许在对象内部访问对象自身的成员变量和成员函数。

注意:this指针不存在对象里。

2、this指针可以为空吗?                                                                                                               

this 指针并不是一个常规的指针,它的值是由编译器隐式地传递的,指向当前对象。因此,在 C++ 中,this 指针通常不会为空,因为它是由编译器在调用非静态成员函数时隐式地传递的,并且指向调用该函数的对象。

尝试在一个空指针上调用非静态成员函数(即在一个未分配对象上调用成员函数),可能会导致未定义行为,这通常会导致程序崩溃或产生未知的结果。因此,this 指针本身不应该为空,否则会导致访问无效的内存地址

注意:this指针确保每个对象都有属于自己的数据成员,但是共享处理这些数据的代码!

3.2、c语言和c++实现Stack的对比

C++中Stack* 参数的编译器维护的(即使用了this指针),c语言中需要用户自己维护。

4、类的六个默认成员函数

当一个类中什么都没有的话就是一个空类。但是空类就是什么都没有吗?答案是:并不是

任何类在什么都不写的时候,编译器会自动生成6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数。

初始化和清理:构造函数、析构函数

拷贝赋值:拷贝构造---使用同类对象初始化创建对象

                   赋值重载---主要是把一个对象赋值给另一个对象

取地址重载:主要是普通对象和const对象取地址,这两个很少自己实现。

5、构造函数

5.1、问题:

如果说,在一个类中,通过一个初始化函数 Init 公有方法给对象设置日期,那么在main函数中每次创建一个对象都要去调用该方法设置信息,

Date d1;  d1.Init(2022,7,5);

实在是太麻烦了~

能不能再创建对象的时候就将信息设置进去呢?

那就要引入构造函数!

构造函数并不是开空间创建对象,而是初始化对象。名字与类名相同,创建类 类型 对象时,有编译器自动调用,在对象整个生命周期内只调用一次。

其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器 自动调用 对应的构造函数。
4. 构造函数可以重载。
注意一点:在调用无参构造的时候,
直接写为  Date d1;      而不是  Date d1();    写的时候不要带括号,否则不知道到底是调用无参构造还是声明一个函数。

有一个疑惑:在我们没有写构造函数的情况下,编译器自动生成的默认构造函数有什么用???

当有一个对象调用了编译器自动生成的默认构造函数,但是这个对象的  成员变量依旧时随机值,那这个默认构造函数有什么用呢??

答案是:在c++中,分为内置类型和自定义类型,编译器生成的默认构造函数会对自定义类型成员调用他的默认构造函数。

class Time
{
public:
    Time()
    {
     cout << "Time()" << endl;
    _hour = 0;
    _minute = 0;
    _second = 0;
 }
private:
    int _hour;
    int _minute;
    int _second;
};
class Date
{
private:
    // 基本类型(内置类型)
    //内置类型成员变量在类中声明时可以给默认值。
    int _year = 1970;
    int _month = 1;
     int _day = 1;
    // 自定义类型
      Time _t;
};

int main()
{
    Date d;  //对_t调用他的默认成员函数
    return 0;
}

6、析构函数

对于内置类型,销毁时不需要资源清理,最后系统直接将内存回收即可。但是如果是自定义成员的化,

d 销毁时,要将其内部包含的 Time 类的 _t 对象销毁,所以要调用 Time 类的析构函数。但是: main
// 中不能直接调用 Time 类的析构函数,实际要释放的是 Date 类对象,所以编译器会调用 Date 类的析构
// 数,而 Date 没有显式提供,则编译器会给 Date 类生成一个默认的析构函数,目的是在其内部调用
Time
// 类的析构函数,即当 Date 对象销毁时,要保证其内部每个自定义对象都可以正确销毁
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
补充:两个栈实现一个队列--使用c++的方式自己封装栈,实现上述的oj题,体会编译器生成的析构函数的作用。

最后一点:当类中没有资源申请的时候,析构函数可以不写,直接使用编译器生成的默认析构函数,但是,有资源申请的时候就一定要写,否则会造成资源泄露,比如Stack类。

7、拷贝构造函数

拷贝构造函数只有单个的形参,该形参是对本类类型成员对象的引用(一般使用const修饰),在用已经存在的类 类型对象创建新队形时,由编译器自动调用。

7.1、特性

1、时构造函数的一个重载形式。

2、形参只有一个且必须是类 类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

3、若未显式定义,编译器会生成默认拷贝构造函数(浅拷贝-值拷贝)

下面我们来看一段代码:

class Time
{
public:
 Time()
 {
     _hour = 1;
     _minute = 1;
     _second = 1;
 }
 Time(const Time& t)
 {
     _hour = t._hour;
     _minute = t._minute;
     _second = t._second;
     cout << "Time::Time(const Time&)" << endl;
 }
private:
     int _hour;
     int _minute;
     int _second;
};
class Date
{
private:
    // 内置类型
     int _year = 1970;
     int _month = 1;
     int _day = 1;
     // 自定义类型
     Time _t;
};
int main()
{
     Date d1;
 
 // 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
 // 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
     Date d2(d1);
     return 0;
}
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调 用其拷贝构造函数完成拷贝的。
4、编译器默认生成的拷贝构造函数已将可以完成字节序的值拷贝了,还需不需要我们去自己实现拷贝构造函数呢??
class Stack
{
public:
 Stack(size_t capacity = 10)
 {
     _array = (DataType*)malloc(capacity * sizeof(DataType));
     if (nullptr == _array)
     {
     perror("malloc申请空间失败");
     return;
 }
     _size = 0;
     _capacity = capacity;
 }
 void Push(const DataType& data)
 {
     // CheckCapacity();
     _array[_size] = data;
     _size++;
 }
 ~Stack()
 {
     if (_array)
     {
         free(_array);
         _array = nullptr;
         _capacity = 0;
         _size = 0;
     }
 }
private:
     DataType *_array;
     size_t _size;
     size_t _capacity;
};
int main()
{
     Stack s1;
     s1.Push(1);
     s1.Push(2);
     s1.Push(3);
     s1.Push(4);
     Stack s2(s1);
     return 0;
}

上面代码运行时,程序会崩溃。

分析:**s1调用构造函数,在构造函数中默认申请了10个元素的空间,然后再里面村了4个元素,1,2,3,4

**s2对象使用了s1拷贝构造,而Stack类中没有显式定义拷贝构造函数,所以,编译器会给Stack类生成一份默认的拷贝构造函数,而默认的拷贝构造函数又是值拷贝,所以,会将s1中内容原封不动的拷贝到s2中,因此,s1与s2指向了同一块内存空间。

**当程序退出时,s1与s2要销毁,s2先销毁,销毁时调用析构函数,将这块空间释放了。但是s1并不知道,到s1销毁时,又会释放一次。 一块内存被多次释放,肯定会造成·程序崩溃。

总结:如果类中没有涉及资源申请,拷贝构造函数写不写都可以,一旦涉及资源申请,一定要写拷贝构造函数,否则就是浅拷贝。

7.2、应用场景

1、使用已存在的对象创建新对象

2、函数参数类型为类 类型对象

3、函数返回值类型为类 类型对象

总结:为了提高效率,(如果是传值就需要拷贝一份,所以效率就会低)一般对象传参时,尽量使用引用类型,返回时根据实际场景,能使用引用尽量使用引用。

当函数被调用完之后,返回对象持续存在即可使用引用返回。

在函数调用完时候,以下对象仍然存在:

  1. 静态变量:静态变量在程序的整个生命周期内都存在,即使函数调用完毕,静态变量也会继续存在。

  2. 全局变量:全局变量也在整个程序生命周期内存在,不受函数调用的影响。

  3. 动态分配的内存:如果在函数中使用 newmalloc 分配内存,并返回指向该内存的指针,则分配的内存将在函数调用完毕后继续存在。这种情况下,需要注意手动释放内存以避免内存泄漏。

  4. 静态数据成员:静态数据成员属于类,而不是特定对象的实例,在整个程序生命周期内存在,不受函数调用的影响。

  5. 全局对象:在程序启动时创建的全局对象会在整个程序生命周期内存在,不会因为函数调用的结束而被销毁。

  6. 返回值是引用的对象:如果函数返回的是一个引用,且该引用指向在函数外部创建的对象,则该对象在函数调用完毕后仍然存在。

需要注意的是,在使用动态分配的内存时,必须确保在不再需要时及时释放,以避免内存泄漏。此外,全局变量和静态变量的使用应该谨慎,因为它们可能导致代码的可维护性和可测试性降低。

8、赋值运算符重载

8.1、运算符重载

不能通过连接其他符号来创建新的操作符:比如 operator@
重载操作符必须有一个类类型参数
用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的 this
.* :: sizeof ?: . 注意以上 5 个运算符不能重载。这个经常在笔试选择题中出现。
如果重载为成员函数,那么函数的形参就比操作数看起来少1,如果是全局函数的化,就用友元函数,此时,形参和操作数个数相同。
注意:左操作数是this,指向调用函数对象。

8.2、赋值运算符重载

1、

赋值运算符重载的格式:

**参数类型:const T&, 传引用可以提高传参效率

**返回值类型:T&,返回引用可以提高返回效率。又返回值只为了支持联律赋值。

**检测是否自己给自己赋值

**返回 *this:要符合连续赋值的含义

2、赋值运算符只能重载为类的成员函数不能重载成全局函数。

原因:如果赋值运算符不显式实现,编译器会生成一个默认的。如果在类外自己实现一个全局的就会和编译器默认生成的发生冲突。

class MyClass {
public:
    int x;
    double y;
    char z;
};

MyClass obj1;
MyClass obj2;
obj1 = obj2; // 编译器默认生成的赋值运算符
MyClass& MyClass::operator=(const MyClass& other) {
    if (this != &other) { // 避免自我赋值
        x = other.x;
        y = other.y;
        z = other.z;
    }
    return *this;
}

第一段代码对于编译器来说会自动生成一个默认赋值运算符,类似于第二段代码。

需要注意的是,默认生成的赋值运算符可能不适用于所有情况。例如,如果类中有指针成员,简单的逐成员赋值可能会导致浅拷贝,而不是深拷贝,从而可能导致意外的行为。在这种情况下,通常需要显式定义自定义的赋值运算符,以确保正确的对象复制行为。

3、用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

4、如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现

1、s1对象调用构造函数,在构造函数中,默认申请10个元素的空间,然后存了4个元素。

2、s2对象调用构造函数,在构造函数中,默认申请了10个元素的空间,没有存储元素。

3、Stack没有显式实现赋值运算符重载,编译器会以浅拷贝的方式实现一份默认的。所以,就将一个对象中的内容原封不动拷贝到另一个对象中。

这样做会造成两个问题:

   a、s2原来的空间丢失了

赋值运算符通常在对象已经存在的情况下执行,而不是在对象初始化时。

   b、同一块内存被释放了两次

如果一个类需要显式实现析构函数、拷贝构造函数或赋值运算符中的一个,那么通常也需要实现其他两个。

9、const成员

const 修饰的 成员函数 称之为 const 成员函数 const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中不能对类的任何成员进行修改。

10、取地址及const取地址操作符重载

class Date
{ 
public :
 Date* operator&()
 {
 return this ;
 }
 
 const Date* operator&()const
 {
 return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比 如想让别人获取到指定的内容!

11、再谈构造函数

class Date
{
public:
 Date(int year, int month, int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
private:
 int _year;
 int _month;
 int _day;
};
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化, 构造函数体中的语句只能将其称为赋初值 ,而不能称作初始化。因为 初始化只能初始化一次,而构造函数体 内可以多次赋值

11.1、初始化列表

类中包含以下成员,必须放在初始化列表位置进行初始化:
a、引用成员变量
b、const 成员变量
c、自定义类型成员 ( 且该类没有默认构造函数时 )
代码如下:
class A
{
public:
     A(int a)
     :_a(a)
     {}
private:
     int _a;
};
class B
{
public:
     B(int a, int ref)
     :_aobj(a)
     ,_ref(ref)
     ,_n(10)
     {}
private:
     A _aobj; // 没有默认构造函数
     int& _ref; // 引用
     const int _n; // const 
};
1、尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
2、成员变量在类中声明次序就是其在初始化列表中的初始化顺序。

12、Static成员

特性:

1. 静态成员 所有类对象所共享 ,不属于某个具体的对象,存放在静态区
2. 静态成员变量 必须在 类外定义 ,定义时不添加 static 关键字,类中只是声明
3. 类静态成员即可用 类名 :: 静态成员 或者 对象 . 静态成员 来访问
4. 静态成员函数 没有 隐藏的 this 指针 不能访问任何非静态成员
5. 静态成员也是类的成员,受 public protected private 访问限定符的限制

静态成员函数不属于某个具体的对象,它是与类关联而不是与类的对象关联。因此,静态成员函数没有this指针,因为它不依赖于特定的对象实例。静态成员函数可以直接通过类名来访问类的静态成员变量与静态成员函数,不需要通过对象来调用。

与静态成员函数不同,非静态成员函数是与类的对象实例相关联的,因此它们具有隐含的 this 指针参数,用于指向调用该函数的对象。通过这个 this 指针,非静态成员函数可以访问对象的数据成员和其他成员函数。

static成员变量不能在类的成员函数中引用。---错   可以被引用,只是说不能访问非静态成员。

static成员变量只能在类外进行初始化。

13、友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为: 友元函数 友元类

13.1、友元函数

友元函数 可以 直接访问 类的 私有 成员,它是 定义在类外部 普通函数 ,不属于任何类,但需要在类的内部声明,声明时需要加friend 关键字。
说明 :
友元函数 可访问类的私有和保护成员,但 不是类的成员函数
友元函数 不能用 const 修饰
友元函数 可以在类定义的任何地方声明, 不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同

13.2、友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
1、友元关系是单向的,不具有交换性。
比如上述 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问 Time类的私有成员变量,但想在Time 类中访问 Date 类中私有的成员变量则不行。
2、友元关系不能传递:如果B A 的友元, C B 的友元,则不能说明 C A 的友元。
3、友元关系不能继承,在继承位置再给大家详细介绍。

14、内部类

内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

注意:内部类是外部类的友元。因此,内部类可以通过外部类的对象参数来访问外部类中的所有成员,但是外部类不是内部类的友元。

特性:

1、内部类可以直接访问外部类中的static成员,不需要外部类的对象或者类名。

2、sizeof(外部类)= 外部类,和内部类没有关系。

3、内部类可以定义在外部类的publicprotectedprivate都是可以的。

补充:

错1:

析构的顺序???

析构的顺序与构造的顺序相反:

1、全局的优先于局部的先构造 2、局部的按照出现顺序进行构造,不管static。3、析构的时候先构造的后析构,但是static会改变对象的生存作用域,会放在所有局部对象析构完之后析构。因此:析构顺序是---b a d c

2、赋值拷贝构造函数与赋值运算符重载函数

虽然都是用于对象之间的赋值操作,但是他们的触发条件和使用方法略有不同。前者主要用于对象的初始化和值传递;而后者主要用于对象的赋值操作。在某些情况下,赋值运算符重载的实现可能会调用赋值拷贝构造函数来执行复制操作,但它们是两个独立的概念。

3、区别:

  1. 编译不通过:

    • 编译不通过意味着在编译源代码时发生了错误,编译器无法生成可执行程序或者目标文件。
    • 这种错误通常是由于语法错误、语义错误、类型错误、声明错误等引起的。
    • 编译器会输出错误信息,指示出错的位置和具体的错误原因,开发者需要根据错误信息修改源代码以修复错误。
    • 编译不通过是在编译阶段发生的,程序尚未运行,因此不会生成可执行文件,也不会导致程序崩溃。
  2. 程序崩溃:

    • 程序崩溃是指在程序运行时发生了错误,导致程序异常终止或者崩溃。
    • 这种错误可能是由于内存访问越界、空指针引用、未初始化变量、除零错误等导致的。
    • 程序崩溃会导致程序停止运行,通常会输出错误信息,例如核心转储(core dump)或者运行时错误信息。
    • 程序崩溃发生在运行时阶段,因此编译器无法检测到这些错误,开发者需要使用调试工具或者日志来追踪和解决程序崩溃的原因。
  • 28
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值