C++初阶(类的访问权限以及封装+this指针+构造函数+析构函数+拷贝构造函数+参数列表+友元+内部类)
面向过程与面向对象
C语言是面向过程的,关注的是过程(函数),分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
类的引入与定义
类的引入
C语言中,结构体内部只能定义变量,C++中的结构体不仅可以定义变量,还可以定义函数。也就是说C语言中的结构体在C++中已经升级为类,一般习惯用class关键字定义。
struct student
{
// 成员变量
int age;
int weight;
// 成员函数(方法)
void PrintStudentInfo()
{
cout << "age = " << age << "weight = " << weight << endl;
}
};
类的定义
其中class是定义类的关键字,后面跟着的是类名,“{}”内部是类体,最后结尾跟着是‘;’。
类体中有类的成员,类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
class 类名
{
// 类体:由成员函数和成员变量组成
}; // 分号结尾,和结构体一样
类的定义的两种方式:
1.声明和定义放在一起,全放类体中,此时编译器可能会把成员函数当中内联函数处理
2.声明放在.h的头文件中,定义放在.cpp的文件中,通常这种方法用的比较多
函数重载
概念:函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题。
实现重载的条件:
- 同一个作用域
- 参数个数不同
- 参数类型不同
- 参数顺序不同
// 1.参数个数不同
void Func1(int a, int b)
{
cout << "Func(int s, int b)" << endl;
}
void Func1(int a, int b, int c)
{
cout << "Funv2{int a, int b, int c)" << endl;
}
//2. 参数类型不同
void Func2(int a, int b)
{
cout << "Func2(int a, int b)" << endl;
}
void Func2(char a, int b)
{
cout << "Func2(char a. int b)" << endl;
}
// 3.参数顺序不同
void Func3(char a, double b)
{
cout << "Func3(char a. double b)" << endl;
}
void Func3(double a, char b)
{
cout << "Func3(double a, char b)" << endl;
}
void test01()
{
int a = 10;
double b = 12.33;
char c = 2;
// 参数个数不同
Func1(a, a);
Func1(a, a, a);
// 参数类型不同
Func2(c, a);
Func2(a, a);
// 参数顺序不同
Func3(b, c);
Func3(c, b);
}
调用重载函数的注意:
-
严格的类型匹配,如果类型不匹配,那么尝试转换,转化成功就调用,转换失败就报错
void func(int a) { cout << a << endl; } void test02() { char c = 'c'; func(c);//尝试将字符类型转化成int类型 }
-
函数重载和默认参数一起使用的时候,计算机不知道调用哪个函数
void myfunc(int a, int b = 0)
{
cout << myfunc(int a, int b = 0) <<endl;
}
void myfunc(int a)
{
cout << myfunc(int a) <<endl;
}
void test02()
{
myfunc(10);//二义性问题,不知道调用哪个函数
}
- 函数的返回值不作为函数重载的条件:编译器是通过函数调用时,传入的参数来判断用重载的哪个函数,我们调用函数的时候不需要写返回值,所以返回值不能称为重载函数的条件
类的访问权限以及封装
访问权限
-
在类的内部(作用域范围内),没有访问权限之分。所有成员可以相互访问
-
在类的外部(作用范围外),访问权限才有意义:public
、private、protected
-
在类的外部,只有public修饰的成员才能被访问,在没有涉及继承与派生的时候,private和protected时同等级的,外部不允许访问
访问属性 | 属性 | 对象内部 | 对象外部 |
---|---|---|---|
public | 公有 | 可访问 | 可访问 |
protected | 保护 | 可访问 | 不可访问 |
private | 私有 | 可访问 | 不可访问 |
- public : 修饰的成员在类外可以被访问
- private和protected :修饰的成员类外不可以被访问,只有类内才可以被访问
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- class的默认访问权限为private,struct为public(因为struct要兼容C)
- 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
struct和class的区别
struct可以定义结构体,也可以和class一样定义类,定义方法也是一样的,区别在于struct的默认权限是public,class的默认权限是private
封装
面向对象的三大特性:封装,继承和多态。
封装就是把数据(成员变量)和方法(成员函数)放在了一起,并且进行更严格地管理(访问限定符)。更好地实现类对象之间的交互。
C++对象模型
在C++对象模型中:
- ① 非静态数据成员被放置到对象内部
- ② static数据成员、静态和非静态 函数成员均被放到对象之外
- ③ 对虚函数的支持分为两步:
- a)每个class会为每个虚函数生成一个指针,这些指针统一放在虚函数表中(vtbl)
- b)每个class的对象都会添加一个指针(vptr),指向相关的虚函数表(vtbl)
- vptr的设定(setting)和重置(resetting)都由每一个class的构造函数,析构函数和拷贝赋值运算符自动完成
- ④ 另外,虚函数表地址的前面设置了一个指向type_info类的指针
- C++提供了一个type_info类来获取对象类型信息
也就是说C++的数据和操作是分开存储的,非静态的数据会保存在自己的对象内部,而共享成员函数,多个同类型的对象会共用一块代码。
如图:
提问:那么既然成员函数代码区共享,这块代码是怎么区分是哪个对象调用自己的呢?
C++提供了特殊的对象指针,this指针,this指针指向被调用的成员函数所属的对象。
this指针
概念:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
注意:当一个对象被创建之后,它的每个成员函数都含有一个系统自动生成的隐含指针this,用来保存这个对象的地址,this是C++实现封装的一种机制,它将对象和该对象调用的成员函数链接在一起,在外部看来,每个对象都拥有自己的函数成员,但是其实这些函数成员的代码时共享,传入不同的this指针代表这不同的对象调用成员函数。
this指针保存的是本对象的地址,在this指向的空间内存储了成员变量,而类的成员函数是共享的。
class Date
{
public:
// 隐藏了一个指针参数 Date * const this
void Init(int year, int month, int day)
{
_year = year;// 实际操作==> this->_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
this指针的几个特点:
- this指针的类型是
类名 *const this
,所以this的内容是不允许修改的 - 只能在成员函数内部使用
- this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针(vs存在ecx寄存器中)
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
- 调用成员函数时,不能显示地传实参给this,但可以显示的用
- 静态成员函数内部没有this指针,静态成员函数不能操作非静态成员变量(因为静态成员变量是属于类的而不是属于对象的,静态的变量在编译的时候就已经被创建出来,而this指向的是对象,所以this指向的空间没有存储静态成员变量)
提问:为什么静态成员函数只能使用静态成员变量?
在类中,静态成员可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性。因此,静态成员是类的所有对象中共享的成员,而不是某个对象的成员,所以作为所有对象共享的函数就只能访问共享的变量,不能造成一个对象对另外一个对象的非静态成员造成修改的现象。
类的6个默认成员函数
在一个类中,会默认生成6个默认成员函数,即使是空类也是如此。
分类如下:
构造函数
概念: 一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,一般是用来初始化成员变量,且一个生命周期只调用一次。
构造函数特性:
- 函数名和类名相同
- 无返回值
- 对象实例化是自动调用
- 可以发生重载
class Date
{
public:
// 构造函数
// 1.无参构造函数
Date()
{
}
// 2. 有参构造
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
// 3.全缺省
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
构造函数显示定义的三种方法:
- 无参构造函数(实例化对象时后面不跟括号,否则会变成函数的声明)
- 有参构造函数
- 全缺省构造函数
类中无构造函数时,编译器会默认生成一个无参的默认构造函数。
默认构造函数有三种:全缺省、不含参和编译器给的
在我们不实现构造函数时,编译器就会调用编译器默认生成的构造函数,看下面一串代码,编译器的默认构造函数能否实现成员变量的初始化呢?
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
运行结果如下:
构造函数的调用规则
注意事项1:
如果程序员提供了有参构造,那么编译器就不会提供默认的构造函数,但是编译器还是会给默认的拷贝构造
注意事项2:
如果程序员提供了拷贝构造函数,那么编译器就不会提供默认的构造函数和默认的拷贝构造函数
多个对象的构造函数
- 如果类有成员对象,那么先调用成员对象的构造函数,再调用本身的构造函数
- 析构函数的调用顺序反之(压栈的问题)
- 如果有成员对象,那么实例化对象的时候,必须保证成员对象的构造和析构函数能被调用
class Buick
{
public:
Buick()
{
cout << "Buick的构造函数" << endl;
}
~Buick()
{
cout << "Buick的构造函数" << endl;
}
};
class BMW
{
public:
BMW()
{
cout << "BMW的构造函数" << endl;
}
~BMW()
{
cout << "BMW的构造函数" << endl;
}
};
class Maker
{
public:
Maker()
{
cout << "Maker的构造函数" << endl;
}
~Maker()
{
cout << "Maker的析构函数" << endl;
}
private:
Buick bui;//成员对象
BMW bmw;//成员对象
};
void test()
{
Maker m1;
}
int main()
{
test();
system("pause");
return EXIT_SUCCESS;
}
运行结果如下:
析构函数
概念: 对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
特性:
- 函数名前加一个‘~’
- 无参数,无返回值
- 一个类中有且只有一个析构函数,对象的生命周期结束是,编译器会自动调用
思考:看下面一串代码,他们的构造和析构顺序是怎么样的?
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class B
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
private:
int _b;
};
int main()
{
A a;
B b;
system("pause");
return EXIT_SUCCESS;
}
运行结果如下:
构造的顺序和对象的创建顺序相同,析构和对象的创建顺序相反。(其实就是一个压栈的问题,先进后出)
拷贝构造函数
概念: 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特性:
- 拷贝构造函数是构造函数的一个重载形式
- 参数有且只有一个,且必须使用引用传参,使用传值会引发无穷递归
class Date
{
public:
// 拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
AA _a;
};
下面解释为什么传值会无穷递归
拷贝构造函数调用的时机
情景1:对象以值的方式给函数参数
class Maker
{
public:
//构造函数
Maker()
{
cout << "调用构造函数" << endl;
}
//拷贝构造
Maker(const Maker& d)
{
cout << "调用拷贝构造" << endl;
}
~Maker()
{
cout << "调用析构函数" << endl;
}
};
void func(Maker m)//Maker m = m1;
{
}
void test01()
{
Maker m1;
func(m1);
}
首先Maker m1会调用构造函数,其次当m1作为参数传入的时候,实际上是做了Maker m = m1;会调用拷贝构造,紧接着函数结束,调用两次析构函数。
运行结果如下:
情景2:用一个已有的对象初始化另外一个对象
void test02()
{
Maker m1;
Maker m2(m1);
}
此时Maker m2(m1);会调用拷贝函数
运行结果如下:
情景3:函数的局部对象以值的方式从函数返回
Maker func2()
{
//局部对象
Maker m;
cout << "局部对象的地址:" << &m << endl;
return m;
}
void test03()
{
Maker m1 = func2();//在这里实际上发生的是Maker 匿名对象 = m;Maker m1 = 匿名对象;就是我们介绍的第一种情况,会调用拷贝构造
cout << "m1对象的地址:" << &m1 << endl;
}
首先调用func2函数,在fun2函数中创建局部对象m,会调用一次构造函数,输出局部对象的地址,当执行到return的时候,对象作为函数返回值,实际上调用了拷贝构造(Maker 匿名对象 = m),然后调用Maker m1 = 匿名对象;此时匿名对象进行类型转换,转化为m1,并且func2函数执行结束,释放局部对象,m调用一次析构函数,继续往下执行,输出m1对象的地址,函数执行结束调用析构函数。
运行结果如下:
深浅拷贝
如果未定义拷贝构造函数,编译器会默认生成一个拷贝构造函数,默认的拷贝构造函数会对内置类型按内存存储的字节序完成拷贝,对于自定义类型编译器会调用他们的默认构造函数,这种拷贝是浅拷贝。
浅拷贝有很大的潜在危险,看下面一串代码:
class Student{
public:
Stduent(const char *name, int Age)
{
this->pName = (char*)malloc(strlen(name)+1);
this->age = Age;
}
~Student()
{
cout << "析构函数" << endl;
if(pName!=NULL)
{
free(pName);
pName = NULL;
}
}
public:
char *pName;
int age;
}
void test()
{
Student s1("小花",18);
Student s2(s1);//调用默认的拷贝构造
}
默认的拷贝构造函数会对内置类型按内存存储的字节序完成拷贝,所以仅仅是对存储的内容进行拷贝,在s1和s2中pName指向同一块空间,在调用析构函数释放的时候,一块空间会释放两次,导致程序崩溃
深拷贝解决浅拷贝问题:自己申请一块内存空间,然后把值传递赋给内存空间当中,这样释放的堆区就不会重复。
Student(const Student& stu)
{
cout << "自己的拷贝构造函数" << endl;
//1.自己申请空间
pName = (char *)malloc(strlen(stu.pName)+1);
//2.拷贝数据
strcpy(this->pName, stu.pName);
age = stu.age;
}
构造函数中的初始化列表
初始化列表: 以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class
{
public:
Date(int year, int month, int day)// 成员变量定义处 给变量开空间
: _year(year)
, _month(month)
, _day(day)
{}
private:
// 成员变量声明处
int _year;
int _month;
int _day;
};
注意:
- 每个成员变量只能在初始化列表中出现一次
- 初始化列表是干什么的?指定调用成员对象的某个构造函数
- 类中包含定义时必须初始化的成员时,必须使用初始化列表
- 初始化列表只能写在构造函数
- 如果使用了初始化列表,那么所有的构造函数都需要写初始化列表
- 可以使用对象的构造函数传递数值给成员对象的变量
必须初始化的变量:
- const成员变量(常量)
- 引用成员变量(引用必须在定义时初始化)
- 无默认构造函数(无参、全缺省和编译器给的默认构造函数)的自定义类型成员变量
class AA
{
public:
AA(int a)// AA类无默认构造函数,必须使用初始化列表
{
_a = a;
}
private:
int _a;
};
class Date
{
Date(int year = 1, int month = 1, int day = 1, int i = 10)
:_a(10)
,_ref(i)
,_aa(10)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 成员变量声明的地方 还未申请空间
int _year;
int _month;
int _day;
// 以下三个成员变量都是必须在定义时初始化
const int _a;
int& _ref;
AA _aa;
};
我们一般尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
一个小问题:这段代码的运行结果是什么?
class A
{
public:
A(int a)
:_a2(a)
,_a1(_a2)
{
}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
A a(10);
a.Print();
return 0;
}
运行结果:
解释:
成员变量在初始化列表的初始化顺序与声明顺序有关,和在初始化列表的先后顺序无关。
代码中是 _a1 声明顺序先于 _a2 ,所以 _a2 先给 _a1 赋值,因为此时 _a2 里面放的是随机值,所以 _a1 是随机值,然后10赋值给 _a2 ,所以 _a2 就是10.
explicit关键字
在C语言中,有一个隐式类型转换的概念,相近类型,意义相似的类型可以发生隐式类型转换。例如int和double两个类型都是表示大小的,所以可以发生。那么,在自定义类型中是否可以发生这中转换吗?
先看下面一串代码:
class Date
{
public:
Date(int year, int month,int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "调用构造函数" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1 = { 2022, 1, 21 };
system("pause");
return EXIT_SUCCESS;
}
其中,Date d1 = { 2022, 1, 21 }; 这里其实是编译器先用{ 2022, 1, 21 }构造一个匿名对象,然后把这个对象给d1赋值,因为两次构造是连续的,所以两个动作会被编译器优化为一个构造,两次都是直接构造,但是意义还是不同的。实际上生成了Date对象的临时量。
运行结果如下:
如果我们不想这种隐式类型转换发生,我们一般在构造函数前面加一个explicit的关键字。
explicit Date(int year, int month, int day)
:_year(year)
,_month(month)
,_day(day)
{}
static成员
概念: 声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化。
特性:
- 静态成员属于整个类,且不占类的大小
- 静态成员变量是在类内声明,类外定义的
- 静态成员可以直接通过::指定类域来访问
- 静态成员函数中没有this指针,不能访问非静态的成员
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_sCount++;
_year = year;
_month = month;
_day = day;
}
// 静态成员函数 无this指针,可以直接用::指定类域访问
// 只能访问静态成员变量和函数
static int GetCount()
{
return _sCount;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 成员变量声明的地方
int _year;
int _month;
int _day;
// 静态成员变量属于整个类(所有类)生命周期在整个程序运行期间
// 可以直接用::指定类域访问
static int _sCount;// 类内声明,类外初始化定义
};
// 静态必须要在类外初始化定义
int Date::_sCount = 0;
友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
类的而主要特点之一就是数据隐藏,即类的私有成员无法在类外使用,但是友元打破了这种规则。
友元语法:
- friend关键字只出现在声明处
- 其他类、类成员函数、全局函数都可声明为友元
- 友元函数不是类的成员,不带this指针
- 友元函数可以访问对象任意成员属性,包括私有属性
注意事项:
- 友元关系不能被继承
- 友元关系是单向的。类A是类B的朋友,但是类B不一样是类A的朋友
- 友元关系不具有传递性。类B是类A朋友,类C是类B的朋友,但是类C不一定是类A的朋友
友元函数
class Building
{
//声明这个全局函数为Building类的友元函数
friend void Goodgay(Building& bd)
public:
Building()
{
keting = "客厅";
woshi = "卧室";
}
private:
string keting;
string woshi;
};
void Goodgay(Building &bd)
{
cout << bd.keting << endl;
cout << bd.woshi << endl;
}
void test()
{
Building bd;
Goodgay(bd);
}
运行结果如下:
//类的成员函数称为友元
class Building;//声明类
class GoodGay
{
public:
void func(Building &bud);
};
class Building
{
//声明GoodGay类的成员函数func成为Building类的友元函数
friend void GoodGay::func(Building& bd);
public:
Building()
{
keting = "客厅";
woshi = "卧室";
}
private:
string keting;
string woshi;
};
void GoodGay::func(Building &bd)
{
cout << bd.keting << endl;
cout << bd.woshi << endl;
}
特性:
- 不能用const修饰
- const修饰的是非静态的成员函数
- 一个类可以是很多函数的友元
友元类
友元类,就是让一个类称为另一个类的友元,这样这个类就可以访问另一个类中的所有私有属性。
class Building
{
//声明GoodF类为Building类的友元类
friend class GoodF;
friend class GoodF2;
public:
Building()
{
keting = "客厅";
woshi = "卧室";
}
public:
string keting;
private:
string woshi;
};
class GoodF
{
public:
void func(Building& bd)
{
cout << bd.keting << endl;
cout << bd.woshi << endl;
}
};
//2.通过类内指针访问类的私有成员
class GoodF2
{
public:
GoodF2()
{
cout << "无参构造" << endl;
pbu = new Building;
}
void func()
{
cout << pbu->keting << endl;
cout << pbu->woshi << endl;
}
//拷贝构造
GoodF2(const GoodF2& f2)
{
cout << "拷贝构造" << endl;
//1.申请空间
pbu = new Building;
}
~GoodF2()
{
delete(pbu);
pbu = NULL;
}
public:
Building* pbu;
};
内部类
概念: 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
特性:
- 外部类天生就是内部类的友元类,内部类具有访问外部类私有属性的权限,但外部类却没有访问内部类的私有属性的权限
- 内部类只是受外部类的类域的限制,内部类并不属于外部类,也就是sizeof(外部类)=外部类
class A
{
private:
static int _a;
int _b;
public:
// 内部类
// B天生就是A的友元 B可以访问A的私有成员,但是A不可以访问B的私有
// sizeof(A) = 4 A中成员变量b a属于整个类,不占对象的大小 内部类B和在全局定义的基本一致,只是受A类域的限制
class B
{
public:
void f()
{
}
private:
int _a;
};
};
int A::_a = 10;