7.0 类
类的思想:数据抽象 + 封装
数据抽象是一种依赖于接口和实现分离的编程技术
要实现数据抽象 + 封装,首先得定义一个抽象数据类型
7.1 定义抽象数据类型
// 这是一个类,表面看起来啥也没有,姑且也叫抽象类型吧
class Person
{
};
定义成员函数
class Person
{
void numberID(); // 在类的内部声明函数,就是该类的成员函数
};
class Person
{
void numberID() {} // 这叫类内定义
};
class Person
{
void numberID(); // 此处只声明
};
// 这叫类外定义
void Person::numberID()
{
}
引入 this
this 是什么,按字面意思就是这个
强制记住 this 就是类本身自己固有的一个特殊对象,它是隐藏的,你要记住它是存在的就行了
看下面
class Person
{
int numberID(); // 声明
int m_id; // 成员变量
};
// 这叫类外定义
int Person::numberID()
{
int id = m_id;
return id;
}
class Person
{
int numberID(); // 声明
int m_id; // 成员变量
};
// 这叫类外定义
int Person::numberID()
{
int id = this->m_id; // 跟直接使用 m_id 作用是一样的,记住this是一直存在的
return id;
}
哪个对象调用函数,this就充当调用该函数的对象
class Person
{
public:
int numberID(); // 声明
int m_id; // 成员变量
};
int Person::numberID()
{
int id = this->m_id;
return id;
}
int main(int argc, char* argv[])
{
Person item;
item.numberID(); // 此时 numberID 函数内部的 this 就是 item 对象
return 0;
}
7.1.4 构造函数
类中都会定义它的具体对象被初始化的方式,而这些函数比较特殊,其中就有一个叫构造函数,主要用来初始化对象的数据成员的,它比较特别,只要定义对象,它就会自动执行
class Person
{
public:
Person() {// 这就是构造函数,记住它是没有返回值的啊,而且要跟类名一样
m_value = 0;
}
int m_value;
};
int main(int argc, char* argv[])
{
Person item; // 定义了对象,构造函数自动执行, 上边只要给数据成员 m_value 赋值 0
return 0;
}
默认构造函数
class Person
{
// 内类啥也没有,其实内部编译器会自动创建了默认构造函数,只是你看不到
};
也就说,当我们不显示定义构造函数的时候,编译器就会自动给我们定义一个出来,但你自己只要定义了一个构造桉树,编译器就不会再生成默认构造函数了
class Person
{
public:
Person() { m_value = 10; } // 我们需要对 m_value 赋一个默认值,所以不能让编译器自己生成,就自己定义了
int m_value;
};
有时候看见 = default 是什么含义
class Person
{
public:
Person() = default; // 这样意义是告诉编译器来定义,我就不定义了
int m_value;
};
构造函数初始化列表
class Person
{
public:
Person() :m_value(10) // 构造函数的初始化列表方式
{
}
int m_value;
};
为什么要这么做
class Person
{
public:
Person() :m_value(10) // 构造函数的初始化列表方式
{
m_value = 10; // 我这样不行吗
}
int m_value;
};
还记得对象初始化和赋值的区别吗
class Person
{
public:
Person() :m_value(10) // 这叫初始化
{
m_value = 10; // 这叫赋值
}
int m_value;
};
7.1.5 拷贝、赋值和析构
除了定义类对象的初始化的构造函数外,还有其他的特殊函数呢,既然初始化也该有销毁吧,两个对象间拷贝或赋值的情况吧
好像编译器都能替代我们合成相对应的版本,无需我们操心,但在某些时候,编译器给我们合成的版本会无法使我们正常工作。比如,当有额外需要申请的内存空间时,编译器合成的版本函数就不能为我们提供正常工作了
所以能自定义类的时候能自定义的就自定义吧,不要过分依赖编译器提供的合成版本
7.2 访问控制与封装
之前就一直有疑问
class Person
{
public: // 这个 public 是啥玩意儿
Person() {}
};
这 public 就叫做访问类型符,一共有三个
class Person
{
public: // 1 共有访问
protected: // 2 保护访问
private: // 3 私有访问
};
它的作用域
直至碰到另一个访问控制符或者遇到结尾都是它的作用域
class Person
{
public: // 1 共有访问
/* public 作用域就是这里 */
protected: // 2 保护访问
/* protected 作用域就是这里 */
private: // 3 私有访问
/* private 作用域就是这里 */
};
class Person
{
// 别看它什么都没有,它是有默认访问控制符,那就是 private
};
class or struct
在 C++ 中 class 和struct 都能用来类的,它俩有访问控制权限有点不同
class Person
{
// 默认访问控制权限是 private
};
struct Person
{
// 默认访问控制权限是 public
};
一般地,我都习惯用 class 来定义类,struct 来定义数据类型
class Person
{
public:
int m_value1;
private:
int m_value2;
};
int main(int argc, char* argv[])
{
Person item;
item.m_value1; // 正确,可以调用
item.m_value2; // 错误,m_value2 的访问权限是私有的,类外部的对象无法直接调用哦
return 0;
}
7.2.1 友元
class Person
{
public:
int m_value1;
friend int fun(Person*); // 声明 fun 为友元函数
private:
int m_value2;
};
int fun(Person* p) // 这个不是类成员函数哈,这是友元函数
{
return p->m_value2; // 它竟然能访问到了私有数据 m_value2
}
友元函数的存在会直接破坏掉了现有的规则,本来私有数据不给外部访问的,现在好了,直接暴露出来了,破坏了类的封装
那为什么要用它,可能因为,类内部的有些事情它做不到,它只能借助外力去帮助它了,所以友元就诞生了。
7.3 类定义再探
一个小小的类,感受一下
class Point {
public:
Point(int x = 0, int y = 0):m_x(x), m_y(y){} // 构造函数
// 对外接口函数
public:
inline void set(int x, int y) { m_x = x; m_y = y; } // 建议编译器内联函数
inline int getX()const { return m_x; } // 建议编译器内联函数
inline int getY()const { return m_y; } // 建议编译器内联函数
// 数据成员
private:
int m_x;
int m_y;
};
7.3.3 类类型
class A
{
public:
int m_value;
void fun();
};
class B
{
public:
int m_value;
void fun();
};
别看他们一样,A 和 B 是不同类型的哦
A a;
B b = a; // 错误,A 和 B 类型不相同
类的声明
class A; // 类的声明,也叫前向声明
class A { // 类的声明和定义
}
7.3.4 友元再探
外部函数或类的成员函数都可以成为友元
函数重载也可以成为友元,但需要分别声明
7.4 类的作用域
class A
{
public:
void fun()
{
cout << m_value << endl; // 在类内,可以直接访问成员变量或成员函数
}
private:
int m_value;
};
int main(int argc, char* argv[])
{
A a1;
a1.fun(); // 在类外,只能通过对象去调用,调用的权限受访问限定符的约束
A* a2 = new A(); // 指针的话通过->去调用
a2->fun();
return 0;
}
void A::fun() // 类外定义成员函数时,需要用::去限定
{
// ....
}
7.4.1 名字查找与类的作用域
注意不要重复声明就好了
7.5 构造函数再探
记住构造函数非常重要,以至于现在继续再探
标准库 std::string 也是一个类类型
看它的初始化过程
// 它们能各种各样的初始化方式,得益于构造函数的编写
string foo = "SXTB";
string bar;
bar = "SXTB";
记住初始化列表和函数体赋值的过程,一个是初始化,一个是赋值,如果觉得一样,那举个栗子
声明一个引用的时候,是不是声明和定义一起的,这时候初始化列表的优势就出来了、
int a = 0;
int& b = a; // 引用
b = a; // 你不能这样吧
成员初始化的顺序
class A {
public:
A():b(0),a(b) {}
private:
int a;
int b;
};
你觉得它们的初始化顺序如何,记住,它们的初始化顺序是按照什么的顺序来的
A():b(0),a(b) // 这个顺序无所谓
// 这个顺序才是重要的
private:
int a;
int b;
默认实参和构造函数
符合函数的默认实参规则
7.5.2 委托构造函数
这是 C++11 提供的,在重载的构造函数中,把一些版本的构造函数版本初始化过程委托了给另外版本的构造函数去初始化,当然继承的时候也有用
class A {
public:
A(): A(0, 0){} // 把有参数的构造函数委托给了无参数构造函数去初始化
A(int x, int y) : m_x(x), m_y(y) {}
private:
int m_x;
int m_y;
};
默认构造函数的作用
class A {
public:
A(int a) {}
};
int main(int argc, char* argv[])
{
A a; // 不提供默认构造函数,你不能这样定义对象
A b(1); // 你这样这样定义
return 0;
}
隐形的类类型转换
string = "SXTB"; // 它为什么能这样赋值
string = string("SXTB"); // 这才是它的真貌,编译器会自动执行转换
类类型转换可能不成功
string str = string(1); // 当然你这样是不成功,1 是int类型
抑制隐式转换
隐式转换可能小心会出错,这是时候加上 explicit 就能抑制编译器去隐式转换啦
7.5.5 聚合类
满足以下条件
1、所有成员都是 public
2、没有定义任何构造函数
3、没有类内初始值
4、没有基类,也没有虚函数
struct Point{
int x;
int y;
}
// 使用
Point p = {0, 0};
7.5.6 字面值常量类
是不是想起 constexpr 啦。构造函数不能 const 修饰,但是它可以 constexpr 呀
7.6 类的静态成员
有时候我们不想定义对象而又想调用类的成员怎么办
那么类的静态成员来了,静态成员也受到访问权限的限制哦
class Point {
public:
static int m_x;
static int m_y;
};
// 静态成员一定要在类内声明类外定义!!
// 不然会意想不到的错误
int Point::m_x = 0;
int Point::m_y = 0;
int main(int argc, char* argv[])
{
cout << Point::m_x << Point::m_y << endl; // 直接::使用,无需定义对象
return 0;
}
还有一些区别
class Point {
public:
Point a; // 错误,数据成员必须是完整类型
Point* b; // 正确,指针成员可以是不完整类型
static Point c; // 正确,静态成员可以是不完整类型
};
重中之重,好好理解,往后写程序,你还是会栽跟头在这里面的、
class Point {
public:
void fun1()
{
int a = m_a;// 非静态成员函数可以使用非静态成员变量
int b = m_b;// 非静态成员函数可以使用静态成员变量
}
static void fun2()
{
int a = m_a; // 静态成员函数可以使用静态成员变量
int b = m_b; // 静态成员函数不可以使用非静态成员变量
}
static int m_a; // 静态成员
int m_b; // 非静态成员
};