类:
类的基本思想 | ||||
1、数据抽象和封装 2、数据抽象:一种依赖于接口和实现分离的编程技术 3、封装:实现类的接口和实现分离 首先定义一个抽象数据类型(类的设计者负责考虑类的实现过程,类的使用者只需要抽象地思考类型做了什么,无须了解类型的工作细节) 封装的优点: 1、确保用户代码不会无意间破坏封装对象的状态 2、被封装的类的具体实现细节可以随时改变,无须调整用户级别的代码 | ||||
抽象数据类型 | ||||
1、不允许类的用户直接访问它的数据成员 2、定义在类内部的函数是隐式的inline函数
| ||||
类作用域和成员函数 | ||||
1、类本身就是一个作用域,类的成员函数的定义嵌套在类的作用域之内 2、即便bookNo定义在isbn之后,isbn还是可以使用bookNo,why??? 因为类的定义分两步处理:
| ||||
拷贝函数 | ||||
拷贝、赋值和析构 | ||||
拷贝:初始化变量、以值的方式传递或者返回一个对象等(当初始化一个非引用类型的变量时,初始值被拷贝给变量;返回值用于初始化调用点的一个临时量,该临时量就是函数调用的结果) 赋值:使用了赋值运算符
| ||||
访问控制与封装 | ||||
C++语言中,使用访问说明符加强类的封装性 1、public:整个程序内可被访问,定义类的接口 2、private:可被类的成员函数访问,但不能被类的用户访问,封装了类的实现细节 |
第13章 拷贝控制
对应:
对象拷贝、移动、赋值、销毁(拷贝控制操作) | ||
拷贝构造函数、移动构造函数:定义了用同类型的另一个对象初始化本对象时做什么(创建一个新对象) 拷贝赋值运算符、移动赋值运算符:定义了将一个对象赋予给同类型的另一对象做什么(两个已经存在的对象) 析构函数:定义了当此类型对象销毁时做什么 | ||
拷贝构造函数 | ||
一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,此构造函数为拷贝构造函数 为啥必须是引用??? 如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。 | ||
合成的拷贝构造函数 | ||
1、如果没有为类定义拷贝构造函数,编译器会为我们定义一个。但是不同于合成默认构造函数,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数。 2、合成的拷贝构造函数将每个非static成员拷贝到正在创建的对象中。 3、成员类型决定了如何拷贝:对类类型成员,使用其拷贝构造函数来拷贝,内置类型则直接拷贝;不能直接拷贝一个数组,但合成的默认构造函数会逐元素地拷贝一个数组类型的成员
| ||
拷贝初始化与直接初始化的区别 | ||
形式上: 1、使用=初始化一个变量,执行的是拷贝初始化,编译器把等号右侧的初始值拷贝到新创建的对象中去 2、不使用=,执行的是直接初始化 深层次上: 1、直接初始化,实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数 2、拷贝初始化,要求编译器将右侧运算对象拷贝到正在创建的对象,如果需要还要进行类型转换! 拷贝初始化依赖拷贝构造函数和移动构造函数
拷贝初始化不仅发生在用=定义变量时,还发生在: 1、将一个对象作为实参传递给一个非引用类型的形参 2、从一个返回类型为非引用类型的函数返回一个对象 | ||
拷贝初始化的限制 | ||
见程序 | ||
拷贝赋值运算符 | ||
1、作用:类控制对象如何赋值 2、为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用 | ||
重载赋值运算符 | ||
1、重载运算符本质上是函数,名字由operator关键字后解接表示要定义的运算符的符号组成 2、赋值运算符就是一个名为operator=的函数 3、一个运算符如果是一个成员函数,其左侧运算对象就绑定到隐式的this指针;对于一个二元运算符,右侧运算对象作为显式参数传递 | ||
析构函数 | ||
1、构造函数初始化对象的非static数据成员;析构函数释放对象使用的资源,并销毁对象的非static数据成员 2、析构函数不接受任何参数,因此不能被重载;对于一个给定类,只有唯一一个析构函数 | ||
析构函数完成什么工作 | ||
1、构造函数:有一个初始化部分+函数体,成员初始化在函数体执行之前完成,且按照它们在类中出现的顺序进行初始化 2、析构函数:析构部分+函数体,首先执行函数体,然后销毁成员,成员按照初始化的顺序进行逆序销毁。析构部分是隐式的,销毁依赖于成员的类型:销毁类类型的成员需要执行成员自己的析构函数,内置类型没有析构函数,因此销毁内置类型成员什么也不需要做。 3、隐式销毁一个内置指针类型的成员不会delete它所指向的对象;但是智能指针是类类型,具有析构函数,智能指针成员在析构阶段会被自动销毁。 4、当指向一个对象的引用或指针离开作用域,析构函数不会执行
| ||
什么时候会调用析构函数(见程序) | ||
无论何时一个对象被销毁,就会自动调用其析构函数:
|
//拷贝构造函数
class Foo {
public:
Foo();
Foo(const Foo&);
};
//不能用数组赋值或初始化另一个数组
int a[] = {0,1,2};
int a2[] = a; //错误
a2 =a; //错误
//拷贝初始化与直接初始化
string dots(10, ''); //直接。。。
string s2 = dots; //拷贝
string nines = string(200, '9'); //拷贝。。
//拷贝初始化的限制
//首先,vector的接受单一大小参数的构造函数是explicit
vector<int> v1(10); //正确:直接初始化,寻找最匹配的
vector<int> v2 = 10; //错误
//拷贝赋值运算符
class Foo{
public:
Foo& operator=(const Foo&); //赋值运算符
};
//合成的拷贝构造函数与拷贝赋值运算符等价代码:
Primer P441,P444
//析构函数操作
{ //作用域
//p和p2指向动态分配的对象
Sales_data *p = new Sales_data; //p是一个内置指针
auto p2 = make_shared<Sales_data>(); //p2是一个智能指针
Sales_data item(*p); //拷贝构造函数
vector<Sales_data> vec; //局部对象
vec.push_back(*p2); //拷贝
delete p; //对P指向对象执行析构函数
}
//退出作用域之后,对p2, item, vec执行析构函数
//销毁p2会递减其引用次数;如果引用次数为0,对象被释放
//销毁vec会销毁其元素
1、C++的访问权限说明;编译器的底层是如何实现这些不同类型数据访问权限的;一个派生类对象访问了基类中没有权限的数据会怎么样
C++的访问权限说明? |
public,private,protected public:类的用户都可以访问 private:类的成员、友元可以访问 protected:类的成员、友元、派生类可以访问 |
一个派生类对象访问了基类中没有权限的数据会怎么样 |
报错,如果要访问该成员,方法有:
|
2、C++的多态性的体现;多态有什么用途?写一下虚函数动态绑定的实际C++示例代码;虚函数中的书写最好使用override 关键字,为什么要使用该关键字?虚函数是怎么实现动态绑定的?虚函数表的底层有了解过吗?具体使用了什么数据结构实现;
参考:https://blog.youkuaiyun.com/u013317445/article/details/103498372
//基类
class Animal {
public:
virutal void eat();
};
//派生类
class Cat : public Animal {
public:
void eat() override;
};
class Dog : public Animal {
public:
void eat() override;
};
Cat cat;
Dog dog;
Animal* p = &cat;
p.eat(); //调用cat重写的eat
Animal* p = &dog;
p.eat(); //调用dog重写的eat
3、什么是虚函数?什么是纯虚函数?基类为什么需要虚析构函数?
什么是虚函数? |
|
什么是纯虚函数? |
纯虚函数声明:virtual returntype func(parameter)=0;引入纯虚函数是为了派生接口。 |
基类为什么需要虚析构函数?(Effective c++ 条款7) |
1、polymorphic(带多态性质)base classed应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数 2、classes的设计如果不是作为base classes使用,或者不是为了具备多态性polymorphic,就不该声明virtual析构函数
|
4、struct与class的区别?
struct与class的区别? |
本质区别是访问的默认控制:默认的继承访问权限,class是private,struct是public; |
5、派生类中构造函数,析构函数调用顺序?
派生类中构造函数,析构函数调用顺序? |
构造函数:“先基后派”;析构函数:“先派后基”。 |
6、C++类中数据成员初始化顺序?
构造函数再探(C++ Primer)
构造函数初始值列表 |
如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化 |
构造函数地初始值有时必不可少(程序见primer p258) |
如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值 |
成员初始化顺序 |
1、成员变量在使用构造函数初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与类中定义成员变量的出现顺序有关。 2、类中const、引用、属于某种未提供默认构造函数的类类型的成员常量必须在构造函数初始化列表中初始化 3、类中static成员变量,只能在类外初始化(同一类的所有实例共享静态成员变量)。 |
7、结构体内存对齐问题?结构体/类大小的计算?
8、计算类大小例子
9、友元函数和友元类
参考:友元函数和友元类
友元提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。通过友元,一个不同函数或另一个类中的成员函数可以访问类中的私有成员和保护成员。c++中的友元为封装隐藏这堵不透明的墙开了一个小孔,外界可以通过这个小孔窥视内部的秘密。
友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。
友元函数 |
|
友元类 |
|
注意 |
(1) 友元关系不能被继承。 (2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。 |
10、什么情况下,类的析构函数应该声明为虚函数?为什么?哪些函数不能成为虚函数?
什么情况下,类的析构函数应该声明为虚函数? |
基类具有多态性质时,应该声明virtual析构函数,为什么?防止当基类指针指向派生类对象,用基类指针销毁派生类对象时,只调用基类的析构函数,没有执行派生类的析构函数,导致局部销毁 |
哪些函数不能成为虚函数? |
不能被继承的函数和不能被重写的函数。
|
11、编写一个有构造函数,析构函数,赋值函数,和拷贝构造函数的String类
12、this指针的理解
为啥使用this指针? |
通常在class定义时要用到类型变量自身时,因为这时候还不知道变量名(为了通用也不可能固定实际的变量名),就用this这样的指针来使用变量自身。 |
this指针好处 |
|
13、构造函数初始化列表
构造函数初始值列表 |
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。 |
构造函数初始值列表跟不用初始值列表的区别(见程序) |
|
有的时候必须用带有初始化列表的构造函数 |
|
//显式初始化列表与赋值方式
class Example {
public:
int a;
int b;
//用初始化列表显式初始化类的成员
Example():a(0), b(0.8){}
//一次默认构造,一次赋值
Example(){
a = 0;
b = 0.8;
}
};
14、详解拷贝构造函数相关知识
好文章 |
c++拷贝构造函数详解 |
拷贝构造函数应用的场景: |
|
拷贝构造函数的调用时机 |
1. 当函数的参数为类的对象时 大致步骤: (1)将对象A传入形参时,会产生一个临时变量C (2)调用拷贝构造函数,将A的值传给C (3)执行完函数,析构掉C
大致步骤: (1)先产生一个临时变量xxx (2)调用拷贝构造函数,将返回值temp传给xxx (3)函数执行到最后,先析构temp,然后析构掉xxx
3. 对象需要通过另外一个对象进行初始化 CExample A(C); |
浅拷贝与深拷贝 |
拷贝有两种:深拷贝,浅拷贝。 当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。
浅拷贝:简单的成员赋值 深拷贝:对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,但它们指向的空间具有相同的内容 |
参数传递过程到底发生了什么? |
将地址传递和值传递统一起来,归根结底还是传递的是"值"(地址也是值,只不过通过它可以找到另一个值)! i)值传递: 对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量); 对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);如void foo(class_type obj_local){}, 如果调用foo(obj); 首先class_type obj_local(obj) ,这样就定义了局部变量obj_local供函数内部使用 ii)引用传递: 无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值!而地址总是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型). |
在类中有指针数据成员时,拷贝构造函数的使用? |
如果不显式声明拷贝构造函数的时候,编译器也会生成一个默认的拷贝构造函数,而且在一般的情况下运行的也很好。但是在遇到类有指针数据成员时就出现问题 了:因为默认的拷贝构造函数是按成员拷贝构造,这导致了两个不同的指针(如ptr1=ptr2)指向了相同的内存。当一个实例销毁时,调用析构函数 free(ptr1)释放了这段内存,那么剩下的一个实例的指针ptr2就无效了,在被销毁的时候free(ptr2)就会出现错误了, 这相当于重复释放一块内存两次。这种情况必须显式声明并实现自己的拷贝构造函数,来为新的实例的指针分配新的内存。 |
以下函数哪个是拷贝构造函数,为什么? |
解答:对于一个类X, 如果一个构造函数的第一个参数是下列之一: a) X& b) const X& c) volatile X& d) const volatile X& 且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数. |
一个类中可以存在多于一个的拷贝构造函数吗? |
类中可以存在超过一个拷贝构造函数。
注意,如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化. |
什么情况下必须定义拷贝构造函数? |
当类的对象用于函数值传递时(值参数,返回类对象),拷贝构造函数会被调用。如果对象复制并非简单的值拷贝,那就必须定义拷贝构造函数。例如大的堆栈数据拷贝。如果定义了拷贝构造函数,那也必须重载赋值操作符。 |
X::X(const X&); //拷贝构造函数
X::X(X);
X::X(X&, int a=1); //拷贝构造函数
X::X(X&, int a=1, int b=2); //拷贝构造函数
//多个拷贝构造函数
class X {
public:
X(const X&); // const 的拷贝构造
X(X&); // 非const的拷贝构造
};
15、重载overload,覆盖override,重写overwrite,这三者之间的区别
重载overload,覆盖override,重写overwrite,这三者之间的区别 | ||
1)overload,将语义相近的几个函数用同一个名字表示,但是参数和返回值不同,这就是函数重载 特征:相同范围(同一个类中)、函数名字相同、参数不同、virtual关键字可有可无 2)override,派生类覆盖基类的虚函数,实现接口的重用 特征:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数) 3)overwrite,派生类屏蔽了其同名的基类函数 特征:不同范围(基类和派生类)、函数名字相同、参数不同或者参数相同且无virtual关键字 | ||
overload重载 | ||
1、函数重载可以一定程度上减轻程序员起名字、记名字的负担,编译器会根据实参类型确定调用哪个函数 2、main函数不能重载 3、不允许两个函数除了返回类型外其它所有要素都相同
| ||
重载和const形参 | ||
| ||
const_cast和重载(待补充理解!!!) | ||
1、const_cast常用于函数重载
|
//形参中顶层const被忽略
Record lookup(Phone);
Record lookup(const Phone); //重复声明了Record lookup(Phone);
Record lookup(Phone*);
Record lookup(Phone* const); //重复声明了Record lookup(Phone*);
//底层const实现重载
Record lookup(Account&);
Record lookup(const Account&); //作用于常量的引用
Record lookup(Account*);
Record lookup(const Account*); //作用于指向常量的指针
//因为const不能转换为其它类型,所以只能把const对象传给const形参;但是非常量可以转换成const,所以上面4个函数都作用于非常量对象;编译器优先选择非常量版本的函数
//const_cast去掉常量属性
int main()
{
const int c = 2;
#if 0
int &cc = c;//error: 将int&类型的引用绑定到const int 时,限定符被丢弃
int *pc = &c;//error: const int * 不能初始化int *实体
#endif
#if 1
int & rc = const_cast<int &>(c);
rc = 3;
cout << "after cast" << endl;
//地址相同
cout << &c << endl;
cout <<& rc << endl;
//值不相同
cout << c << endl;
cout << rc << endl;
#endif
system("pause");
return 0;
}
16、静态绑定和动态绑定的介绍
1)对象的静态类型和动态类型 静态类型:对象在声明时采用的类型,在编译时确定 动态类型:当前对象所指的类型,在运行期决定,对象的动态类型可变,静态类型无法更改 2)静态绑定和动态绑定 静态绑定:绑定的是对象的静态类型,函数依赖于对象的静态类型,在编译期确定 动态绑定:绑定的是对象的动态类型,函数依赖于对象的动态类型,在运行期确定 |
17、引用是否能实现动态绑定,为什么引用可以实现
指针或引用是在运行期根据他们绑定的具体对象确定。 |
18、子类析构时要调用父类的析构函数吗?
派生类的析构函数在执行完后,会自动执行基类的析构函数,这个是编译器强制规定的,没有为什么,甚至你在析构函数里调用return都不会立即返回到调用处,而是会先按顺序把析构函数全部调用完。 |
19、有哪几种情况只能用intialization list 而不能用assignment?(3)
|