一、类和对象入门概念
C++大佬最终是用struct来引入类的。
C语言结构体只能定义成员。
C++兼容C的用法。
但是C++同时对Struct进行了升级。把struct升级成了 类。
struct 名称。
升级成了类的意思是什么呢?
1.结构体 名称 可以定义类型。
2.里面可以定义函数。
这两者都是C语言不能写的。
但是C语言不可以这么写。
C++结构体名称定义的变量,可以调用里面的函数。
C++里面并不是必须加(下划线)_这个符号的。只是习惯。是用来标识成员变量的。C++并没有命名规定,看公司。
注意:在类中,类是一个整体,所以说成员变量随意放在类中的任何位置。
1.类class
虽然C++可以用struct来定义类,但是更习惯用class来定义类。
在C++中,结构体名称定义的变量,他不叫变量了。他叫对象!
结构体变量 = 对象。
类有两部分构成
:成员函数和成员变量。
2.类的访问限定符
C++ 引入了访问限定符。
作用是:限定在对象外对成员进行访问。
访问限定符:
访问限定符到下一个访问限定符出现是他的范围。
1.public
struct默认是共有的。可以在class类外面访问。struct也可以加访问限定符。
类中的成员默认为私有的。只有加上public才能共有的。
2.prvivate:
3.protect:
现学习阶段,我们认为保护 和 私有 是一样的。也是在类外面无法访问。
等到学到继承的时候再来探讨。
1.C语言数据和方法是分离的。
这样太自由了。这样容易出错。
2.但是C++是数据和方法都是在类中的。
数据和方法都封装在了一起。一般封装到一起后的第一件事是。
把不想让访问的定义为私有,把让访问的设置为共有。
这样在软件工程中,叫做高内聚,低耦合。
在一般情况下,设计类,成员数据都是私有或者保护,想给访问的函数是公用,不想给你访问时设为私有或保护。比如实现增容接口。外界没必要调用。所以可以为私有的。
C++封装的意义是什么?
C++的封装设计是更严格的管理。
3.类的作用域
类其实是新定义了一个作用域。所以这就可以解释,为什么成员变量可以定义在函数的下面。
类内定义和声明, 类外实现。
在类中定义的函数默认为内联inline函数。
一般情况下。但不是规定。实际中,短小的函数在类里面定义。长一点的函数声明和定义分离。
4.类的实例化
变量的声明和定义的区别在于:有没有开辟空间。
声明就是不开,定义就是开辟空间。
实例化出来的对象才能存数据。所以说类中的变量是声明的,不能为其赋值。
5.类对象的存储方式
只计算其中的成员变量所占的空间。不算其成员函数。
我们可以用栈 实例化出 多个对象。
对象调用类中的函数是 同一个函数。但是各自的成员变量的指针得各自存一份,因为成员变量的值都不同!。
所以说我们可以在将函数放在公共的区域里,这个区域叫做代码段。
对于没有成员变量的类对象,编译会给他们分配1byte占位,表示对象存在过。
内存对齐规则复习!
二、什么是面向对象?
C语言:是面向过程的,关注的是过程。然后分析出求解问题的步骤,通过函数调用逐步解决问题。
C++:是面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成的。
例如:我们以外卖系统为例子。我们平时用的美团外卖,外卖由 商家,骑手,用户三个部分组成,这三个部分就是对象,每个对象有不同的任务。例如商家负责做饭,装饭。骑手负责接单,抢单,送外卖。用户负责点外卖,吃饭。
总结:面向对象可以理解为商家,骑手,用户这三个对象各自干自己的分内的事情。然后三个对象通过交互组成一个系统。
三、什么是类?
类可以说是对C语言的升级。C++的类是根据C语言的struct传承而来的。
C语言中,结构体中只能定义变量,不能定义函数。
而在C++中,为了弥补这个缺陷,结构体内不仅可以定义变量,也可以定义函数。
以下是类的定义的代码。
class 类名
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
1
2
3
4
5
class是类的关键字,类名是给用户起的。花括号{}里面是类的主体,由成员函数和成员变量组成。
类中的元素称为类的成员。
类的成员:包括类的成员变量和类的成员函数。但通常习惯将变量称为类的属性,将函数称为类的方法。
注意:分号一定不要忘加,在Linux系统的vim编辑器中,分号不会自动补充。而在vs中会自动补充。所以在Linux中一定要小心。
四、类的定义方式
类有两种定义的方式。但一般采用,类内声明,类外定义的这种方式。
方式1:类内声明,类外定义,这种一般都是声明放在.h文件中,类的定义放在.cpp文件中。
.h文件
class Date
{
public:
//只写类的声明
void Print();
private:
int _year;
int _month;
int _day;
};
1
2
3
4
5
6
7
8
9
10
.cpp文件
//这里面写函数的定义
void Date::Print()
{
cout << _year << " "
<< _month << " "
<< _day << endl;
}
1
2
3
4
5
6
7
方式2:声明和定义放一起。
class Date
{
public:
void Print()
{
cout << _year << " "
<< _month << " "
<< _day << endl;
}
private:
int _year;
int _month;
int _day;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
五、类的作用域
在类内声明函数,类外定义函数的话,需要用作用域限定符:: 指明成员函数属于拿个类。
例如:指明了Print()成员函数属于Date这个类。
void Date::Print()
{
cout << _year << " "
<< _month << " "
<< _day << endl;
}
1
2
3
4
5
6
六、类的实例化(对象)
什么是类的实例化?类的实例化:就是创建对象。
代码如下。创建对象的代码在main函数中。
class Date
{
public:
//打印日期
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//创建对象d1
Date d1;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
七、如何理解类实例化出来的对象?
理解之前需要知道什么是类。类的代码部分就是class Date{}这一块的。
1.类就是一个模具。里面是类的成员。定义出来的类并没有分配内存空间。
2.类是模具。所以可以根据模具创建多个对象。实例化出来的对象,占用实际的物理空间,存储类成员变量,注意 不存储成员函数。
3.类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图。
八、对象大小的计算
1.类对象的存储方式
因为每个对象中成员变量是不同的,但是调用同一份函数,如果每个对象都保存一份代码,相同代码保存多次,浪费空间。
结论:所以,对象的存储方式是,只有变量占有内存空间,而成员函数则放在了公共代码段。
2.对象大小计算
结论:一个类的大小,实际就是该类中“成员变量”之和,当然也要进行内存对齐。
注意:应该注意空类的大小,空类比较特殊,编译器给了空类一个直接来唯一标识这个类
3.结构体内存对齐规则
和C语言结构体规则相同。
1.第一个成员在结构体偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的对齐数 和 该成员变量大小的 最小值。
vs中默认的对齐数是8
3.结构体总大小为:编译器默认的对齐数 和 该成员变量的 对齐数 最大值的整数倍。
4.如果嵌套了结构体。嵌套的结构体对齐到自己的最大对齐数的整数倍,结构体的整体大小是最大对齐数的整数倍。
九、类成员函数的this指针
1.什么是this指针?
代码举例
class Date
{
public:
//打印日期
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//创建对象d1
Date d1;
Date d2;
d1.Init(2022,7,7);
d2.Init(2022,7,8);
d1.Print();
d2.Print();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
对于上面代码。
Date类中有Print和Init两个成员函数,在调用d1.Print 和 d2.Print两个函数的时候没有传参。但是当d1调用Print函数的时候,该函数怎么知道是d1对象调用的,而不是d2对象调用的?
结论:
C++通过this指针解决这个问题。C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象,在函数调用的时候编译器自动完成。不需要用户显示传参。
2.this指针的特点
1.this指针的类型为:类的类型 + const例如:Date const
2.只能在成员函数内部使用
3.this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
我们平时写的代码
void Print()
{
cout<< _year << endl;
}
1
2
3
4
5
编译器实际处理时的代码。
void Print(Date* const this)
{
cout<< this-> _year << endl;
}
1
2
3
4
5
3.this指针可以为空吗?
结论:可以为空指针。
举例:
class Date
{
public:
//打印日期
void Print()
{
cout << _a << endl;
}
void show()
{
cout << "show()" << endl;
}
private:
int _a;
};
int main()
{
Date* p = nullptr;
p->Print();//运行崩溃
p->show();//正常运行
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
这里的p是空,所以this也为空,因为对象中只有成员变量,成员函数在公共代码段,所以show()可以正常运行。但是Print崩溃,原因是this->_a空指针解引用崩溃。
十、类的访问限定符和封装
1.什么是访问限定符?
访问限定符:有 public, private, protected。
C++实现封装的方式:用类将对象的 属性 和 方法 结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部用户使用。
1.public修饰的成员在类外可以直接访问
2.protected 和 private修饰的成员只能在类内访问,在类外需要加 域限定符::
3.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止。
4.class的默认权限为private,struct的默认访问权限为public(因为struct要兼容C语言)
2.什么是封装?
封装:将数据 和 操作数据的方法 进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
十一、类的4个默认成员函数
如果一个类中什么也没有,那就叫做空类。任何一个类在我们不写的情况下,会自动生成下面6个默认成员函数。
六个默认成员函数:分别为构造函数,析构函数,拷贝构造函数,赋值重载函数,普通对象取地址函数,const对象取地址函数。
1.构造函数
构造函数:构造函数是一个特殊的成员函数,名字和类名相同,创建类类型对象时有编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期只执行一次。
代码举例:
class Date
{
public:
//无参构造函数
Date()
{}
//带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;//调用无参构造函数
Date d1(2022,7,12);//调用有参构造函数
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
默认构造函数
默认构造函数只是一个名词,默认构造函数有三种。
1.我们没有写,编译器自动生成的默认构造函数。
2.无参的构造函数
3.全缺省的构造函数
注意:默认构造函数只能有一个。
编译器自动生成的默认构造函数有什么用?
C++把类型分成内置类型和自定义类型。
内置类型是: int/char/double等。
自定义类型是:我们用class/struct/union自己定义出来的类型。
我们不写,编译器自动生成的默认构造函数:对内置类型不做处理,对自定义类型调用它的默认构造函数。
代码举例:
class Time
{
public:
Time()
{
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2.析构函数
概念:析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
析构函数特点:
1.析构函数名是在类名前加上字符 ~
- 无参数无返回值。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
代码举例:
显式写出来的析构函数
typedef int DataType;
class SeqList
{
public :
SeqList (int capacity = 10)
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));
assert(_pData);
_size = 0;
_capacity = capacity;
}
~SeqList()
{
if (_pData)
{
free(_pData ); // 释放堆上的空间
_pData = NULL; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private :
int* _pData ;
size_t _size;
size_t _capacity;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
默认生成的析构函数
我们不写presson类的析构函数,我们会看到编译器生成的默认
析构函数,对会自定类型成员调用它的析构函数。
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
class Person
{
private:
String _name;
int _age;
};
int main()
{
Person p;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
3.拷贝构造函数
概念:用已经拥有的对象创建一个不存在的对象。叫做拷贝构造。
拷贝构造函数特点:
1.拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
代码举例:
用d1拷贝d2
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
浅拷贝
概念:若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,叫做浅拷贝,或值拷贝。
我们假如不写拷贝构造函数,编译器会自动生成一个默认拷贝构造函数,但是只能进行值拷贝。假如是一块地址就不行了。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//不写拷贝构造函数,编译器会自动生成一个默认拷贝构造函数。
/* Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}*/
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
深拷贝
概念:简单来说,只要类属性里有指针等就必须利用深拷贝操作。
重点:因为浅拷贝是值拷贝。有指针的话,只会拷贝一个相同的指针,这样就出现了两个指针指向同一块堆区上的内存。这样在程序结束自动调用析构函数时,会析构两次,造成程序崩溃。
以下程序没有显示写深拷贝。结果析构两次发生了错误。
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
}
s1和s2的_str都指向了同一个内存空间。最后释放析构了两次,发生崩溃。