一、类(class)
1.类的构造函数
-
每一个类都有属于他自己的构造函数
-
无论何时只要类的对象被创建,就会执行构造函数
-
构造函数没有返回类型、构造函数不能被声明为const
编译器创建的构造函数又被称为合成的默认构造函数。如果类内存在初始值
那么就用他来初始化成员,否则默认初始化
如果自己定义了类,那么类就不会执行默认构造函数,除非自己声明他
####
只有当类没有声明任何构造函数时,编译器才会自动地生成默认地构造函数
-
如果类包含有内置类型或者复合类型地成员,则只有当这些成员全部都被赋予了类内地初始值时,这个类才适合于使用合成的默认构造函数 -----?
Sales_data和Number_data 都是类 如果Sales_data(Number_data &nd)这种的构造函数,只有Number_data有初始值,才能被定义。
定义Sales_data的构造函数
class Sales_data{
Sales_data() = default;
Sales_data(const string &s):bookNo(s);
Sales_data(const string &s,size_t n,double p):bookNo(s),units_sold(n),revenue(p*n){ }
Sales_data(istream &);
}
上述代码我们定义了四个构造函数,其中default含义为默认执行,也就是默认构造函数
第二条则是一个初始化构造函数,叫做构造函数初始化列表
Sales_data(const string &s) : bookNo(s); 冒号前面是类型,后面是把s这个形参传递给需要初始化的对象
这个与上面的一样
在类的外部定义构造函数
-
当我们在类的外部定义构造函数时,必须指明该构造函数是哪个类的成员,即Sales_data::Sales_data;
2.类的友元函数
因为在类中的私有部分,不是类的内部函数是不能访问类的内部的私有变量的,所以外部函数在想调用内部的成员变量时,可以使用声明为友元函数来使用类的内部成员变量。
在头文件中friend声明一下就可以了
小知识点:关于istream和ostream在类中的重载
类似于string类中的getline,可以看到我们有两个形参,第一个是需要输入输出的输出流,第二个是需要输出输入的对象,我们的返回值是引用类型的目的是,方便连续的输出输入
3.拷贝、赋值和析构
除了定义类的对象如何初始化之外,类还需要控制拷贝、赋值和销毁对象时发生的行为。当我们不主动定义这些操作时,编译器将替我们合成它们。一般来说,编译器生成的版本将对对象的每个成员执执行拷贝、赋值和销毁的操作。
这里先不细讲,之后的章节会提供。注意的时,我们能定义这些函数就尽量去自己定义。
有指针类型不支持默认拷贝类型---------浅拷贝
指针赋值给指针时,只是指针指向了内存空间,两个对象公用一个内存空间。
深拷贝,需要再开辟内存空间
4.类的其他特性
-
定义类型别名,定义类型别名,必须先定义后使用
-
如果我们定义了构造函数,需要把默认构造函数写出来
4.1内联函数
在类中,常有一些小规模的函数适合于被声明成内联函数。定义在类内部的成员函数是自动inline的。内联函数和类放在同一个头文件中
如何定义内联函数?
-
在头文件的函数声明前面用inline声明
-
在头文件中直接定义函数
-
在函数体前加inline声明
内联函数定义在内外都可以,但是函数体必须要简单,不能太复杂
4.2重载成员函数
和非成员函数一样,成员函数也可以被重载,只要函数之间在参数的数量和类型上有所区别就行。
4.3可变数据成员
可以通过在变量声明中加入mutable关键字做到这一点
一个可变数据成员永远不会是const,即使他是const对象的成员。
5.this指针
调用某个成员函数,this指针就指向函数本身,*this也就代表着函数本身
以上函数可以实现连续调用
Screen s1; s1.move(4,0).set('x').display()
原因:
函数的返回值为Sceen& 为类本身! 如果是Sceen那么会大不相同 Screen返回的就是类的一个副本,本不是类的本身,结果虽然一样,但是创建了多了Sceen的副本。
-
从const成员函数返回的*this,this指针会自动变为常量指针
-
如果this指针在函数体中指向了常量函数,那么指针也会被隐式的转换成常量指针
6.类的类型
-
一个类的成员变量不能够是该类自己!
-
如果是类本身的指针或者引用,被声明过之后不会报错。
-
类名就是类型
不完全类型
什么是不完全类型?
不完全类型就是在程序中引入类时,我们知道他是一个类,但是不知道他到底包含哪些成员。
不完全类型可以以不完全类型作为参数和返回值
7.友元再探
友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明。
不但有友元函数,还有友元类
window_mgr就是Screen的友元类,一旦成为友元类,Window_mgr中的成员函数都可以访问Screen中的成员函数。
-
友元不存在传递性
也就是说window_mgr如果有友元函数,window的友元函数不能访问screen中的成员
-
友元可以单独声明一个函数
友元函数被声明后,在类中还是不能够直接调用该函数,甚至就算在类中定义该函数,我们也必须在类的外部提供相应的声明从而使函数可见。换句话说,即使我们仅仅是用声明友元的类的成员调用该友元函数,他也必须是被声明过的。
8.类的作用域
使用作用域可以使编译器知道该函数的作用域在哪,可以访问类中的成员
-
先声明类名
-
编译器处理完类中的全部声明后才会处理成员函数的定义
-
类型名需要特殊处理
-
内层作用域可以重新定义外层作用域
-
成员函数的形参不要和函数使用同样的名字
注意:
例如如下函数
在类的外层定义的type是string 然而在Exercise类中定义的类型为double
那么setval函数返回的type类型是string 而initval函数返回的类型是double,就会造成类型不匹配的问题
所以,本小节就是提醒我们要注意类的作用域中的类型,防止问题的报错,在调用类的函数体或者定义一个类的一个函数的时候,要注意作用域的使用
9.构造函数再探
什么时候用赋值?什么时候用成员初始化列表?
就对象的数据而言,初始化和赋值也有类似的区别。如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。
但是当成员变量为const和引用、某种未提供未初始化的构造函数需要使用成员初始化列表。
提供默认构造函数
成员初始化列表的顺序---不限定初始化的顺序---跟定义的顺序有关--------成员声明的顺序一致
默认构造函数有两种形式,一个是不带参数,一个是带参数但全部提供了默认值
如果提供两个默认构造函数提供初始值,会有二义性
委托构造函数
Sales_data():Sales_data("",0,0){}
委托构造函数,就是将任务委托给冒号之后地构造函数,执行这个构造函数并先执行此构造函数,函数体内的语句
`Sales_data(const string &s,double p,double n) : bookNo(s),price(p),units_sold(n){cout <<"1) world";}
Sales_data():Sales_data("66-77-88-99",10.00,20.00){cout <<"2) hello";}`
如果调用无参数值的构造函数,那么先打印1后打印2
9.1.默认构造函数的作用
当我们没有自己编写构造函数时,编译器会自动生成一个构造函数,编译器创建的构造函数又称为合成的默认构造函数。
只有没有其他构造函数时,编译器才会提供,一旦有,编译器则不会提供
默认合成构造函数生成条件:
-
当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时
-
当一个类本身含有类类型的成员且使用合成的默认构造函数
-
当类类型的成员没有在构造函数初始值列表中显式地初始化时
-
在数组初始化的过程中如果我们提供的初始值数量少于数组的大小时
-
书写vector<T> name(10) 容器时,当T为一个类时,会自动调用默认构造函数
在实际中,如果定义了其他的构造函数,最好也定义一个默认构造函数
生成b时会自动调用A的构造函数
9.2隐式的类型转换
-
只允许一步类类型转换
-
形参只有一个
9.2.1、explicit的作用
-
explicit阻止隐式的类类型转换,但是使用显式类型转换也是可以用的
-
explicit没必要在多个参数的构造函数上加
-
explicit只能用于直接初始化
-
只要在类的定义中加explicit就可以
-
如果不希望构造函数进行隐式的转换,最好加上explicit
例如:
vector 的构造函数定义为 explicit:
通常情况下,vector 是用来存储一组元素的容器,其单参数构造函数可能传递一个元素值或者指定容器大小。将其定义为 explicit 可以避免隐式类型转换,确保只有在明确需要时才会调用该构造函数。 避免了不必要的隐式类型转换可能会减少代码中的歧义,提高代码的清晰度和可维护性。 string 的构造函数没有定义为 explicit:
string 类被设计为在字符串处理中提供更加方便的操作,这意味着在很多情况下会自动进行类型转换。在字符串处理中,隐式转换通常是一个很常见且实用的特性。 为了便捷性和简洁性,可能会选择不将 string 的构造函数定义为 explicit,以便在代码中更自然地使用字符串。 总的来说,对于 vector 这样的容器类,显式地定义构造函数可以帮助避免潜在的错误和提高代码的健壮性;而对于 string 这样的字符串类型,隐式类型转换可以提高代码的易用性和可读性。这种设计选择通常是为了在不同情况下平衡使用的便捷性和类型安全性。
9.3、聚合类
-
所有的成员都是public
-
没有定义任何构造函数
-
没有类内初始值
-
没有基类,也没有虚函数(virtual)
字面值常量类
字面值,指算术类型、引用和指针。
-
数据成员都是字面值类型的聚合类
-
数据成员都是子面试类型,类必须至少有一个constexpr的构造函数
-
如果数据成员有初始值,那么他必须是一条常量表达式;如果成员属于类类型,那么类类型的初始值必须使用成员自己constexpr构造函数
constexpr构造函数
1.constexpr的函数体必须是空的,return语句除外
2、成员为字面值类型,返回类型为constexpr类型
字面值常量类的作用可以使其接收两个及以上的字面值类型,把类搞成一个字面值的常量去使用,不允许他被更改
总的来说,constexpr
的主要使用场景是在需要在编译时确定常量值的情况下,以及在需要在编译阶段对函数进行求值的情况下。这有助于提高代码的性能,减少运行时计算以及在编译阶段实现更多的优化。
10.类的静态成员
我们通过在成员的声明之前加上关键字static使得其与类关联在一起一样,静态成员可以是public或者private的,静态数据成员的类型可以是引用、指针、类类型等。类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据
-
静态成员不能在类内部进行初始化,必须在类外部进行初始化且只能定义一次
-
静态成员函数不能声明成const
-
static const int a = 10;允许这样定义一个常量,enum{a = 10}与枚举一样
-
不能重复使用static关键字
-
静态成员可以定义类类型