目录
类的基本思想是数据抽象和封装,数据抽象是一种依赖于接口和实现分离的编程技术,封装就是实现了类的接口和实现的分离。
1. class类
1.1 访问说明符
public:成员可以在整个程序被访问,一般定义类的接口。
private:成员只能在类内被访问,一般封装类的实现细节。
但是也不一定说,public里只定义函数,private里只定义变量,只是说private里定义的成员,只能在类内被访问,即提供给public里的成员访问,这就有很好的封装性了,实现细节在private的函数里实现,在public的函数里使用。
1.2 class和struct的区别
class和struct唯一区别就是访问权限的不同,struct默认访问权限是public,class默认访问权限是private。
1.3 类和内联函数
定义在类内的函数都是内联的,类外默认情况下不是内联的。
内联函数的使用:一般来说,内联函数适用于那种使用次数多,操作量小的函数,所以能够定义为内联函数的函数,最好在类内就进行实现,或者在类内声明为内联,类外实现也行。同时如果在类外定义inline函数,则必须将类定义和成员函数的定义都放在同一个头文件中(或者写在同一个源文件中)
1.4 类的声明(☆☆☆)
class Screen;
在定义类之前,声明类,称为向前声明,是一种不完全类型,在未定义这个类之前,其只能用来:
1.定义指向这种类型的指针或引用。
class Screen;
class Window_mgr
{
private:
Screen *screen;
};
class Screen
{
};
2.以不完全类型作为参数或者返回类型的函数。
class Screen;
class Window_mgr
{
public:
void func(Screen &sc);
};
class Screen
{
};
就是不能用这个不完全类型来访问此类的成员。
class Screen;
class Window_mgr
{
public:
void func() { screen->x = 10; } // err 报错,因为不能在未定义这个类之前访问这个类的成员
private:
Screen *screen;
};
class Screen
{
public:
int x;
int y;
};
1.5 名字查找和类的作用域
名字查找的过程:
1.先在名字所在块中寻找其声明语句,只在名字使用之前出现的声明
2.没找到,则找外层作用域
3.最终没找到,则程序报错
类的编译过程:
1.首先,编译成员的声明
2.直到类全部可见后,才编译函数体
所以,类内变量的使用可以使用在声明之前,如:
class ABC
{
public:
int balance() {return val;}
private:
int val;
}
所以,也存在覆盖问题,如果类外与类内有同名的定义,则会被类内的覆盖。除类型名外,因为类型名不允许重复定义,不会覆盖,但会报错。
总而言之是个由内往外的过程。如下所示:先在(1)中查找,没找到再(2),没找到再(3)。优先级也是从(1)到(3),即如果(1)中有要查找的名字的申明,则优先使用(1)中的,包括形参。
(3)...
class ABC
{
void func() { (1)... }
(2)...
}
2. this指针
class Person
{
public:
int getnumber() {return number;}
private:
int number;
};
Person ps;
cout<<ps.getnumber()<<endl;
ps是如何调用到number成员的?
因为有this指针,每一个成员函数都会有一个this指针,它是一种隐式的参数。
本质是:classname *const this 因为this总是指向类的对象,所以是一个常量指针。
所以,ps的调用实质上是:
getnumber(Person *const this);
Person::getnumber(&ps); //ps.getnumber()的实质 ,这是个伪代码,只是为了说明
this的作用:
1.this指针可以用来返回类的对象本身
Person func()
{
return *this;
}
3. const常量成员函数
常量成员函数:const修饰的成员函数,只能访问,不能修改成员变量。
int getnumber() const {return number;}
const修饰成员函数,其实是修饰this指针,即:
Person *const this; ----{经过const修饰后}----> const Person *const this;
目的是:为了让类的常量对象也能访问这个成员函数。
因为:常量对象不能调用非常量成员函数,非常量对象所有成员函数都能调用!!
int getnumber() {return number;}
const Person p;
p.getnumber(); //false,常量对象不能访问非常量成员函数
//因为:Person *const this 不能赋值给 const Person &p;
//它们两个不具有相同的底层const,所以不能赋值,this就不能初始化。左右两个都只有顶层const.
int getnumber() const {return number;}
const Person p;
p.getnumber(); //true
4. this指针与const成员函数
问题:假设我们需要在类Screen中写一个display函数来将类中的一些内容陈列出来,并且要返回类本身和因为不需要对类进行任何改变,所以类要求是常量成员函数,即display() const ,又因为要返回对象本身,而此时this指针是一个指向常量的常量指针,所以返回的必须是const Screen &,类名:const Screen &display() const {....} ,这样就导致一个问题,此时我们的对象是一个非常量对象,它调用完这个返回常量对象的成员函数,又怎么能去调用非常量成员函数呢?
解决办法:const重载,重载display函数。
class Screen
{
public:
const Screen &display(std::ostream &os) const { do_display(os); return *this; }
Screen &display(std::ostream &os) { do_display(os); return *this; }
void getXY();
private:
......
void do_display(std::ostream &os) const { os << this->x; os << this->y; }
};
Screen sc;
sc.display(os).getXY(); //调用的是第二个
const Screen sc;
sc.display(os).getXY(); //调用的是第一个
5. mutable关键字
作用:如果你想在const成员函数里修改某成员变量,则需要用mutable关键字修饰此成员变量。
如:当你想要记录某函数的调用次数的时候。
class Screen
{
public:
void modifyxy(int X) const { x = X; }
private:
mutable pos x;
};
6. 构造函数
class Person
{
public:
Person() = default;
Person(string &m_name, string &m_address) : money(1000) { name = m_name; address = m_address; }
Person(string &m_name) : address("1asdf"), money(1000) {}
};
6.1 默认构造函数
默认构造函数将按照如下规则初始化类:
1.如果类内成员存在初始值,则用它来初始化成员。
2.如果没有,则默认初始化该成员。
默认构造函数的作用:初始化成员变量,但若某些类缺少默认构造函数,则默认构造函数不会成功,你要自己实现默认构造函数。所以,最好每一个类必须要写一个默认构造函数。
6.2 =default
如果类中没有写构造函数,那么编译器就会执行默认的构造函数,但如果类中写了构造函数,那么编译器将不会执行默认的构造函数。
当我们需要其它构造函数,又需要默认的构造函数时,就可以在参数列表后加上 = default 来要求编译器生成构造函数。
Person() = default;
使用场景:当你使用类时只有小部分情况下才通过定义的多参构造函数进行构造时,这样你大部分情况下使用默认构造就行了。
6.3 构造函数初始值列表
Person(string &m_name, string &m_address) : money(1000) { name = m_name; address = m_address; }
Person(string &m_name) : address("1asdf"), money(1000) {}
Person(string &m_name) : address("1asdf"), money(1000),arr{0,1,2,3,4,5} {} //使用列表初始化得用{}
“:” 后面加成员变量名,“()” 里填写初始化值,"{}"是列表初始化,同时,变量之间用 “,” 隔开。
当类内定义了引用、const或者属于某种未提供默认构造函数的类类型时,就必须要通过构造函数的初始化列表来给这些成员提供初始值。
class Screen
{
public:
Screen(int a=0):r(a)
{
}
private:
int x;
int y;
public:
int &r;
};
//未提供默认初始化的类
class NoDefault
{
public:
NoDefault(int) {}
};
class C
{
public:
C():nd(0) {}
NoDefault nd;
};
初始化顺序与类定义中出现的顺序一致!与初始化列表的前后顺序无关!
class X
{
int i;
int j;
public:
X(int val):j(val),i(j) // err,会优先初始化i,而此时j还未初始化,所以,i(j)就是错误的
{}
};
6.4 委托构造函数
委托构造函数:它使用它所属类的其他构造函数执行自己的初始化过程。
class Sales_data
{
public:
explicit Sales_data(std::string number, std::string bname, std::string aname, int v) :bookno(number), value(v), bookname(bname), author(aname) { }
Sales_data():Sales_data("2022-10-12", "queue", "wfkln", 100) {}
Sales_data(std::string number, std::string aname,int value):Sales_data(number,"king",aname, value){}
private:
std::string bookno;
std::string bookname;
std::string author;
int value;
};
如上所示,第二个,第三个就是委托构造函数,它们把初始化委托给了第一个构造函数执行。
作用:避免重复书写列表不同,逻辑相似的构造函数。
注意:
1.不能出现”委托环“
所谓 委托环(delegation cycle) 是指某类中有一个若多个委派构造函数, 然后在这些若干个的委派构造函数中, 某些目标构造函数可能同时是委派构造函数。 这样一来, 委派构造函数形成了一个链状结构(如链表的首尾相连情况.), 这就是所谓的“委托环”。
2.构造函数不能同时“委派”和使用初始化列表委托构造函数详解,小白也可以看懂_CodeBowl的博客-CSDN博客_委托构造函数
6.5 隐式的类类型转换(转换构造函数)
如果构造函数只接受一个实参,那么它实际上定义了转换为此类类型的隐式转换机制。
比如:
Sales_data
{
public:
Sales_data()=default;
Sales_data(const string &s) {...}
Sales_data &combine(const Sales_data&) {...}
......
};
Sales_data item;
string null_book="9-9999-999";
item.combine(null_book);
在 item.combine(null_book); 的地方就发生了隐式转换,转换过程如下:
string null_book="9-9999-999";
item.combine(null_book);
Sales_data temp(null_book); //首先,会产生一个临时对象temp;
item.combine(temp); //其次,item调用combine函数,传入的参数实际上是这个临时对象temp
不过,隐式转换必须符合:只允许一步类类型转换的规则如:
string null_book="9-9999-999";
item.combine("9-9999-999"); //我这里不传入null_book,直接传入"9-9999-999"
// err,因为:这里首先const char*类型要准换为string类型,再转换为sales_data类型,
//这里就发生了两次类型转换,不符合一步类类型转换规则
item.combine(string("9-9999-999")); // 这样就是正确的
6.6 explicit关键字
explicit关键字就是用来抑制上文的隐式转换的,在构造函数前面加上explicit关键字,就可以避免这样的隐式转换,如:
class Sales_data
{
public:
Sales_data()=defalut;
explicit Sales_data(const string &s) {}
Sales_data &combine(const Sales_data& sd) {...}
};
Sales_data item;
string null_book="9-9999-999";
item.combine(null_book); // err 因为构造函数是explicit的
// 实现细节:
// const Sales_data& sd=null_book; //这里会执行构造函数,Sales_data(const string &s)
// //但是因为有explicit,所以不允许这样的隐式转换,即不允许执行拷贝形式的初始化
// //会报错
其实简而言之,explicit关键字修饰的构造函数,只能用于直接初始化,不允许使用"="来调用构造函数。
Sales_data item1(null_book); // true
Sales_data item1=null_book; // err
注意:
1.explicit关键字只对一个实参的构造函数有效。需要多个实参的构造函数不能用于隐式转换
2.explicit关键字只能在类内的构造函数申明处出现。
虽然explicit关键字不允许隐式转换,但是我们可以显式转换。
item.combine(Sales_data(null_book));
含有单参数的构造函数:
string:不是explicit的,接受单参数const char*的构造函数
vector:是explicit的,接受一个容量参数的构造函数
7. 友元
友元的申明可以在类内的任何地方,并不一定要在类和非成员函数的声明之后,比如:
class X
{
friend void f();
x() { f(); } // err,f还没有声明
}
void f();
因为:友元声明的作用:只影响访问权限,本身没有普通意义的声明。
7.1 友元函数
类可以允许函数访问类的对象的非公有成员,方法是使用 friend 关键字修饰类或函数。
class Person
{
friend Person addmoney(Person &p1, Person &p2);
friend istream &read(istream &i, Person &p1);
friend ostream &print(ostream &o, Person &p1);
private:
string name;
string address;
int money = 0;
};
注意:
1.友元函数不是类的成员函数,不受类的访问权限的控制,所以可以在类内任意地方声明。
2.有些编译器要求 friend 修饰的函数必须要在修饰前进行声明,有些编译器不需要,所以,最好是在类前先声明下,friend 要修饰的函数。
7.2 友元类
把类定义成友元:允许友元类的成员访问此类的私有部分。
class Window_mgr;
class Screen
{
friend Window_mgr;
private:
int x;
int y;
};
class Window_mgr
{
public:
void func() { screen->x = 10; }
private:
Screen screen; //Screen *screen
};
访问此类的私有部分的方式:
1.通过实例对象或指针对象访问。
2.通过形参形式访问。
7.3 友元成员函数
把类的成员函数定义成友元:允许另一个类的成员函数访问此类的私有部分。
class Screen;
class Window_mgr
{
public:
void clear();
private:
Screen *screen;
};
class Screen
{
friend void Window_mgr::clear();
public:
......
private:
int x;
int y;
};
void Window_mgr::clear()
{
screen->x = 0;
screen->y = 0;
}
把某个成员函数作为友元的步骤:
step1. 首先定义Window_mgr类,其中要申明clear函数。
step2. 定义Screen类,包括对clear函数进行友元声明。
step3. 定义clear成员函数。
为什么要这样?因为:如果要在clear成员函数里使用Screen的私有成员,则必须要在clear函数实现之前就定义Screen,第一步使用的是Screen的不完全类型,不完全类型是不能对其类的成员进行访问的。
8. 聚合类
聚合类:使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。
聚合类(格式类似于枚举)必须符合如下规则:
1.所有成员都是public的
2.没有定义任何构造函数
3.没有类内初始值
4.没有基类,也没有虚函数
struct data
{
int ival;
string s;
}
class data
{
public:
QString A;
QString B;
QString C;
QString D;
QString E;
QString F;
QString G;
QString H;
}
使用 "{}" 初始化成员,初始值的顺序必须与申明的顺序一致。
Data vall={ 0 , "Anna" } ; //true
Data vall={ "Anna" , 1024 } ; //err 顺序错误
9. 字面值常量类
字面值常量类必须符合如下规则:
1.数据成员必须都是字面值类型(什么是字面值类型[C++]const 限定符和 constexpr 关键字_Asphyxia+的博客-CSDN博客)
2.类必须至少含有一个constexpr构造函数
3.如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式,或者如果成员属于类类型,那么这个类类型必须提供了一个constexpr构造函数。
4.类必须使用析构函数的默认定义。
class Debug
{
public:
constexpr Debug(bool b=true):hw(b),io(b),other(b){}
constexpr Debug(bool h,bool i,bool o):hw(h),io(b),other(o){}
constexpr bool any() {return hw||io||b;}
void setio(bool b) { io = b; }
void sethw(bool b) { hw = b; }
void setother(bool b) { other = b; }
private:
bool hw;
bool io;
bool other;
};
10. 类的静态成员(★★★)
静态成员属于类不属于对象,是所有对象共享,十个对象也只有一个变量,节省空间时间。静态成员存储在静态存储区,该区域中的数据在整个程序的运行期间一直占用这些存储空间,也就是内存地址不会变,所以静态变量会一直存在着。【这个知识其实就限定了静态成员的很多问题】
10.1 static和const的区别
1.static修饰的变量的生命周期是整个程序,而const修饰的变量生命周期是在作用域内。
2.static可以修改,const不能修改。
3.static const和const static是没有什么区别的。
4.静态成员对象可以访问所有成员函数和成员变量,常量成员对象可以访问所有成员变量,但只能访问常量成员函数。
5.静态成员函数只能访问静态成员变量,常量成员函数也只能访问常量成员函数(因为常量成员函数里调用成员函数,默认的对象是常量对象,即this指针是由const修饰的,所以只能访问常量成员函数),但可以访问所有成员变量。
10.2 申明和定义
静态成员变量注意:要在类内声明,类外初始化,且要定义在任意函数之外!!因为:类内申明,并不会为静态成员分配空间,类只有在生成实例化对象的时候才会分配空间,所以要定义在类外且独立于任意函数之外,这样才能为静态成员分配空间,并让其生命周期保持于整个程序。
//.h
class Account
{
public:
static void Modifystatic(int j);
private:
static void func();
static int k;
};
//.cpp
int Account::k; //定义在任意函数之外
void Account::Modifystatic(int j)
{
Account::k = j; //初始化
}
10.3 静态成员的访问方式
1.类名+作用域访问
class Account
{
private:
static int k;
};
int Account::k=10;
2.通过对象访问
class Account
{
public: //这里得是public权限
static int k;
};
int Account::p; //注意这里也必须得全局定义
Account ac;
ac.k=10;
10.4 静态成员和普通成员的区别
1.静态成员可以是不完全类型,而普通成员不可以。同时引用和指针类型也可以是不完全类型。
class Account
{
public:
......
private:
static Account mem1; //true
Account* mem2; // true
Account mem3; // err,数据成员必须是完全类型
};
2.静态成员可以作为默认实参,普通成员不能作为默认实参。
class Account
{
public:
void clear(char k = bk);
private:
static const char bk;
};
10.5 静态成员函数
静态成员函数作用:
1.只能访问静态成员变量,没有this指针
2.可以不用创建对象就能调用。
class A
{
public:
static A* GetAInstance(); //获取实例对象
......
static int GetCount(); //获取生成对象的次数
private:
static A* a;
static int count;
};
int A::count = 0;
A::A()
{
count++;
}
A* A::GetAInstance()
{
if(!a)
{
a = new A();
}
return a;
}
int A::GetCount()
{
return count;
}
int main()
{
A* instance = A::GetAInstance();
cout<<A::GetCount(); //不需要生成对象就能调用
}
10.6 注意
1.静态成员也必须符合类的权限限定!静态成员变量也只能在类外进行初始化,而不能使用。
2.静态成员函数只能访问静态成员变量。因为普通成员变量在没有实例化对象前是没有存储空间的,此时也就只能在静态成员函数里使用静态成员变量了。
3.static const和const static可以在类内初始化,也可以在类外初始化。