目录
思维导图大纲:
const成员函数
- 1.我们称被const修饰的成员函数为const成员函数,const修饰在函数后面
- 2.const成员函数内部的this指针,将由A* const this 变成 const A* const this
- 3.由原本的不可以更改*this指向的对象,变为即不可以更改*this指向的对象又不可以更改*this对象的内容
//举例
class A
{
public:
// A(A* const this, int a = 0) const修饰this,我们不可以更改this指向的对象
A(int a = 0)
{
// this->_a = a;
_a = a;
}
// 在函数Print中,我们传参A* const this,如果没有const修饰,那么在该函数可以对对象进行修改
// 如果加上const修饰,传参过去相当于const A* const this,对于普通对象属于权限缩小,对于const对象属于权限平移
// 我们平时成员函数不对成员变量进行修改时最好加上const修饰
// void Print(const A* const this) const
void Print() const
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
// 定义普通对象aa1 传参过去相当于A* const this
A aa1(1);
aa1.Print();
// 定义const对象aa2 传参过去相当于const A* const this
const A aa2(2);
aa2.Print();
return 0;
}
取地址运算符重载
由以下代码我们可以看见&运算符是获取对象的存放地址,
使用编译器默认生成的即可,如果自己去实现更改可能会造成地址不符合的情况
class Data
{
public:
// 对于普通对象
Data* operator&()
{
return this;
}
// 对于const修饰的对象
const Data* operator&()const
{
return this;
}
private:
int _Date;
};
再探构造函数-初始化列表
1.一开始我们对于成员变量初始化是基于赋值下,而初始化列表是对成员变量进行直接的初始化
我们知道初始化一个变量是可以不给值的
class Data
{
public:
/*Data(int data = 0)
{
_Date = data;
}*/
// 以上的构造函数相当于
/*int _Date;
_Date = data;*/
// 使用初始化列表
Data(int data = 0)
:_Date(data)
{}
// 以上的构造函数相当于
// int _Date = data;
// 打印函数
void Print()const
{
cout << _Date << endl;
}
private:
// 变量声明:
int _Date;
};
int main()
{
Data d(1);
d.Print();
return 0;
}
2.每个成员变量在初始化列表只能出现一次
如果出现多次就相当于我们
int a = 10;
再次 int a = 10;
class Data
{
public:
// 使用初始化列表
Data(int data = 0)
:_Date(data)
//,_Date(data) err
{}
private:
// 变量声明:
int _Date;
};
3.对于const修饰的成员变量我们只有初始化这一次机会修改他的值
对于引用成员变量我们必须初始化
对于没有默认构造的自定义类我们也需要初始化
class A
{
public:
// 没有默认构造函数
// 以下是一个正常的构造函数,需要传参
A(int a)
:_a(a)
{}
private:
int _a;
};
class Data
{
public:
// 使用初始化列表
Data(int& a, int data = 0)
:_Date(data) // 初始化正常变量
, _D(1) // 初始化const修饰变量
, _da(a) // 初始化引用成员变量
, _aa(1) // 初始化自定义类型(没有默认构造函数)
{}
void Print()
{
cout << _Date << endl;
}
private:
// 变量声明:
// 正常变量
int _Date;
// const修饰
const int _D;
// 引用成员变量
int& _da;
// 自定义类型(没有默认构造函数)
A _aa;
};
int main()
{
int i = 0;
Data dt(i);
dt.Print();
return 0;
}
4. 成员变量声明处的缺省值用于的是初始化列表
class Date
{
public:
//Date(int year = 1, int month = 1, int day = 1)
// :_year(year)
// ,_month(month)
// ,_day(day)
//{}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 注意这⾥不是初始化,这⾥给的是缺省值,这个缺省值是给初始化列表的
// 如果初始化列表没有显⽰初始化,默认就会⽤这个缺省值初始化
int _year = 1949;
int _month = 10;
int _day = 1;
};
int main()
{
Date d;
d.Print();
return 0;
}
5.尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,
如果这个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化。
如果你没有给缺省值,对于没有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构造会编译错误。
6.初始化列表中按照成员变量在类中声明顺序进⾏初始化,
跟成员在初始化列表出现的的先后顺序⽆关。建议声明顺序和初始化列表顺序保持⼀致。
class A
{
public:
A(int a)
:_a(a)
,_b(_a)
{}
void Print() {
cout << _a << " " << _b << endl;
}
private:
int _b = 10;
int _a = 10;
};
int main()
{
A aa(1);
aa.Print();
return 0;
}
// 以上程序会输出什么? 答案: 1 随机值
隐式类型转换
c语言中我们了解:
int main()
{
// 比如我们定义一个int类型,使用double类型接收,
// 会产生类型转换,生成一个临时变量,临时变量具有常性
int i = 0;
double j = i;
int m = 0;
const double& n = m; // 对于常量的引用需要加上const
return 0;
}
c++中:
单参数
// 类型转化在c++中的应用(单参数) c++98支持
class A
{
public:
A(int a)
:_a(a)
{}
A(const A& a)
{
cout << "拷贝构造" << endl;
_a = a._a;
}
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
// 平常构造
A aa1(1);
aa1.Print();
// 转换构造
// 2构造⼀个A的临时对象,再⽤这个临时对象拷贝构造aa2
// 编译器遇到连续构造+拷贝构造->优化为直接构造
A aa2 = 2;
aa2.Print();
// 对于引用也是如下:
A& ra1 = aa2;
const A& ra2 = 2;
return 0;
}
多参数
// 对于多参数(c++11支持)
class A
{
public:
A(int a, int b)
:_a(a)
,_b(b)
{}
void Print()
{
cout << _a << " " << _b << endl;
}
private:
int _a;
int _b;
};
int main()
{
A aa1(1, 1);
aa1.Print();
A aa2 = { 2,2 };
aa2.Print();
const A& ra1 = { 2,2 };
return 0;
}
防止类型转换
// 如果不想让类型转换发生,在构造函数加一个关键字explicit
class A
{
public:
explicit A(int a)
:_a(a)
{}
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A aa1(1);
aa1.Print();
// A aa2 = 2; // err
// aa2.Print();
return 0;
}
// 有了类型转换我们使用起来就更加方便,来看以下代码
class A
{
public:
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
class Stack
{
public:
void Push(const A& aa)
{
//....
}
private:
A _arr[10];
int _top;
};
int main()
{
Stack st;
// 以下代码是等价的
A aa1(1);
st.Push(aa1);
st.Push(1);
return 0;
}
static成员
• ⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化。
• 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
• ⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
• 静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。
• ⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
• 突破类域就可以访问静态成员,可以通过(类名::静态成员)或者(对象.静态成员)来访问静态成员变量和静态成员函数。
• 静态成员也是类的成员,受public、protected、private访问限定符的限制。
• 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,
静态成员变量不属于某个对象,不⾛构造函数初始化列表。
class A
{
public:
A(int i = 0)
{
++_i;
}
~A()
{
++_i;
}
A(const A& aa)
{
++_i;
}
static int GetACount()
{
return _i;
}
private:
static int _i;
};
// 静态成员函数需要在类外面进行初始化
int A::_i = 0;
int main()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
cout << a1.GetACount() << endl;
return 0;
}
OJ练习:
求1 + 2 + 3 + ... + n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A ? B : C)。
class Sum
{
public:
Sum()
{
_sum += _i;
++_i;
}
// 静态成员函数,没有this指针
static int GetSum()
{
return _sum;
}
private:
static int _sum;
static int _i;
};
int Sum::_sum = 0;
int Sum::_i = 1;
class Solution {
public:
int Sum_Solution(int n) {
Sum* p = new Sum[n];
delete[] p;
return Sum::GetSum();
}
};
友元
友元分为:友元函数和友元类
在函数声明或者类声明的前⾯加friend关键字,并且把友元声明放到⼀个类的里面
友元函数可以访问类的私有成员变量,但是友元函数不是成员函数,只是一种声明
一个函数可以是多个类的友元函数
友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。
友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。
友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是c的友元。
有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多⽤。
class A
{
// 友元声明
friend void Print(const A& aa);
public:
A(int a = 1, int b = 1)
:_a(a)
, _b(b)
{}
private:
int _a;
int _b;
};
void Print(const A& aa)
{
cout << aa._a << " " << aa._b << endl;
}
int main()
{
A aa(1, 2);
Print(aa);
return 0;
}
class A
{
// 友元声明
friend class B;
public:
A(int a = 1)
:_a(a)
{}
private:
int _a = 1;
};
class B
{
// 友元声明
friend class A;
public:
B(int b = 2)
:_b(b)
{}
void Print(const A& aa)
{
cout << aa._a << " " << _b << endl;
}
private:
int _b = 2;
};
int main()
{
A aa;
B bb;
bb.Print(aa);
return 0;
}
内部类
• 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。
内部类是⼀个独⽴的类,跟定义在全局相⽐,他只是受外部类类域限制和访问限定符限制,
所以外部类定义的对象中不包含内部类。即类的大小不受内部类影响。
• 内部类默认是外部类的友元类。
• 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,
那么可以考虑把A类设计为B的内部类,如果放到private / protected位置,
那么A类就是B类的专属内部类,其他地⽅都⽤不了。
class Solution {
public:
int Sum_Solution(int n) {
sum* p = new sum[n];
delete[] p;
// 变长数组
// sum arr[n];
return _sum;
}
private:
static int _sum;
static int _i;
// 内部类
class sum
{
public:
sum()
{
_sum += _i;
++_i;
}
private:
};
};
int Solution::_sum = 0;
int Solution::_i = 1;
匿名对象
class Solution
{
public:
Solution(int ret = 10)
:_ret(ret)
{}
void print()
{
cout << _ret << endl;
}
private:
int _ret = 10;
};
int main()
{
// 平时调用
Solution s1(1);
s1.print();
// 匿名对象
Solution().print(); // 生命周期只在这一行,用完就销毁
return 0;
}
// 以下是匿名对象的应用
#include <algorithm>
int main()
{
int arr[] = { 5,9,4,86,21,35,48,62 };
// < 升序#include <algorithm>
int main()
{
int arr[] = { 5,9,4,86,21,35,48,62 };
// < 升序
sort(arr, arr + 8);
// 循环for
for (int& ret : arr)
{
cout << ret << " ";
}
cout << endl;
// > 降序
sort(arr, arr + 8, greater<int>()); // 匿名对象
// 循环for
for (int& ret : arr)
{
cout << ret << " ";
}
return 0;
}
对象拷贝时的编译器优化
• 现代编译器会为了尽可能提⾼程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传参过程中可以省略的拷贝
class Date
{
public:
// 默认构造函数
Date(int year = 1949, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{
cout << "默认构造函数" << endl;
}
// 析构函数
~Date()
{
//...
cout << "析构函数" << endl;
}
// 拷贝构造
Date(const Date& d)
{
cout << "拷贝构造" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
// 赋值运算符重载
Date& operator=(const Date& d)
{
cout << "赋值运算符重载" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
void Print() const // const Date* const this
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year = 1949;
int _month = 1;
int _day = 1;
};
void fun1(Date d)
{}
Date fun2()
{
Date d;
return d;
}
int main()
{
// 传值传参
// 由于fun1的参数不是Date&,会形成一次拷贝构造
// 这边打印的应该是(d1的构造函数)+(d1传参的拷贝构造)
// 然后是析构函数*2
Date d1;
fun1(d1);
// 如果是隐式类型转换,连续的构造+拷贝构造->编译器优化成直接构造
// 然后是析构函数
fun1({ 2024, 1, 1 });
// ⼀个表达式中,连续的构造+拷贝构造->优化为⼀个构造
fun1(Date(2024, 1, 1));
// 传值返回
// 函数表达式中先构造d,然后在构造一个临时对象拷贝构造d进行返回
// 但是在一些编译器上会直接优化一个拷贝构造,减少一个拷贝
// ⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,直接变为构造。
fun2();
// 如果我们又去接受返回值
// 直接构造d,构造临时对象进行拷贝构造d,
// 返回接收的dd又进行一次直接构造+拷贝构造。
//返回时⼀个表达式中,连续直接构造+拷贝构造->优化⼀个拷⻉构造
//⼀些编译器会优化得更厉害,进⾏跨⾏合并优化,直接变为构造。
Date dd = fun2();
// ⼀个表达式中,连续拷贝构造+赋值重载->⽆法优化
Date dd;
dd = fun2();
return 0;
}