1. 面向对象
1.1 描述面向对象技术的基本概念
包括以下几点内容:
- 类:具有相似的内部状态和运动规律的实体集合。
- 对象:指现实世界中各种各样的实体,也就是类的实例。
- 消息:指对象间相互联系和相互作用的方式。一个消息主要由5个部分组成:发送消息的对象、接受消息的对象、消息传递办法、消息内容、反馈。
- 类的特性:抽象、继承、封装、重载、多态。
答案:
面向对象是指按人们认识客观世界的系统思维方式,采用基于对象(实体)的概念建立模型,模拟客观世界分析、设计、实现软件的办法,包括类、对象、消息以及类的特性等方面的内容。
1.2 c++与c语言相比的改进
答案:
C++ 是从C语言发展演变过来的。C语言是过程式编程语言,它以过程为中心、以算法为驱动。而C++ 能够使用面向对象的编程方式,即使用以对象为中心、以消息为驱动的编程方式。这是C++在C语言的基础上最大的改进。
1.3 class与struct有什么区别
这里有两种情况下的区别:
- C语言的struct与C++的class的区别
- C++中的struct与class的区别
在第一种情况下。struct与class有着非常明显的区别,C语言中的struct是一种数据类型,struct中只能定义成员变量,不能定义成员函数。
在第二种情况下。它们之间唯一的不同就是默认的权限,class默认的权限是private的,而struct是public。
答案:
- C语言的struct与C++的class的区别:struct只是作为一种复杂数据类型定义,不能用于面向对象编程。
- C++中的struct和class区别:对于成员访问权限以及继承方式,class中默认的是private,而struct中则是public。class还可以用于表示模板类型,struct则不行。
1.4 C++类成员的访问
struct 中所有行为和属性都是 public 的(默认)。C++中的 class 可以指定行
为和属性的访问方式。
封装,可以达到,对内开放数据,对外屏蔽数据,对外提供接口。达到了信息
隐蔽的功能。
答案:
- 需要被外界访问的成员直接设置为public。
- 只能在类内访问的成员设置为private。
- 只能在基类和子类中访问的成员设置为protected。
1.5 类成员的初始化
#include<iostream>
using namespace std;
class obj {
public:
obj(int k) :j(k), i(j) {
}
void print() {
cout << i << endl << j << endl;
}
private:
int i;
int j;
};
int main() {
obj o(2);
o.print();
return 0;
}
初始化列表的初始化顺序与变量的声明顺序一致,而不是按照出现在初始化列表中的顺序。这里成员i比成员j先声明,因此代码的顺序是先用j对i进行初始化,然后用2去初始化j。
1.6 静态成员的使用
#include<iostream>
using namespace std;
class test {
public:
static int i;
int j;
//test(int a) :i(1), j(a) {}; static成员只能在类内定义,类外初始化。
test(int a) : j(a) {};
void func1();
static void func2();
};
void test::func1() { cout << i << "," << this->j << endl; }
//void test::func2() { cout << i << "," << j << endl; }//error 非静态成员引用必须与特定对象相对
void test::func2() { cout << i << endl; }
int main() {
test t(2);
t.func1();
t.func2();
return 0;
}
1.7 与全局对象相比,使用静态数据成员有什么优势
- 静态成员没有进入到全局名字空间,因此不存在程序中与其他全局名字冲突的可能性。
- 使用静态成员可以隐藏信息。因此静态成员可以是private成员,而全局对象不能。
1.8 全局对象的构造函数在mian函数之前执行
#include<iostream>
using namespace std;
class test {
public:
test() {
cout << "constructor of test" << endl;
}
};
test a;
int main() {
cout << "main fun start" << endl;
test b;
return 0;
}
输出结果:
答案:
全局对象的构造函数在main函数执行之前运行。
1.8 C+++中的空类默认会产生哪些类成员函数
编译器会默认生成如下的函数:
- 默认构造函数和拷贝构造函数。
- 析构函数。
- 赋值函数(同类的对象间赋值的过程)。
- 取值运算。对类的对象进行取地址(&)时,此函数被调用。
1.9 构造函数和析构函数是否可以被重载
构造函数可以被重载,因为构造函数可以有多个,且可以带参数。
析构函数不可以别重载,因为析构函数只能有一个,且不能带参数。
1.10 重载构造函数的调用
#include<iostream>
using namespace std;
class test {
public:
test(){}
test(const char *name,int len=0){}//1
test(const char *name){}//2
};
int main() {
test obj("Hello");//error 有多个重载构造函数与实例匹配
return 0;
}
由于构造函数1的第二个参数有默认的值,所以创建对象obj的时候调用的构造函数具有模糊语义,编译器无法决定调用哪个。
1.11 构造函数的使用
#include<iostream>
using namespace std;
struct CLS {
int m;
CLS(int j) :m(j) {
cout << "CLS(int):this = " << this << endl;
}
CLS() {
CLS(0);
cout << "CLS() : this = " << this << endl;
}
};
int main() {
CLS obj;
cout << "&obj = " << &obj << endl;
cout << obj.m << endl;
return 0;
}
答案:
首先CLS obj生成一个对象,调用obj的构造函数CLS(),生成一个临时对象,临时对象调用它的构造函数CLS(int) ,打印它的地址,然后obj的构造函数打印obj的地址,接下里main函数里面打印obj的地址以及obj的成员m,所以输出结果中2,3行的地址是obj的地址,第一行是临时对象的地址。
输出结果:
1.12 构造函数explicit 与普通构造函数的区别
explicit构造函数是用来防止隐式转换的。
#include<iostream>
using namespace std;
class Test1 {
public:
Test1(int n) { num = n; }
private:
int num;
};
class Test2 {
public:
explicit Test2(int n) { num = n; }
private:
int num;
};
int main() {
Test1 t1 = 12;
//Test2 t2 = 12; error
Test2 t3(12);
return 0;
}
Test1的构造函数带一个int型的参数,代码Test t1=12会隐式转换成调用Test1的构造函数。而Test2的构造函数被声明为explicit(显式),这表示不能通过隐式转换来调用这个构造函数。
1.13 C++中虚析构函数的作用是什么
析构函数是为了在对象不被使用之后释放它的资源,虚函数是为了实现多态。那么,把析构函数声明为virtual有什么作用呢?
#include<iostream>
using namespace std;
class base {
public:
base() { cout << "base 构造函数调用" << endl; };
/*virtual ~base() {
cout << "output from the destructor of class base" << endl;
}*/
~base() {
cout << "output from the destructor of class base" << endl;
}
virtual void DoSomething() {
cout << "Do something in class base" << endl;
}
};
class Derived :public base{
public:
Derived() { cout << "Derived 构造函数调用" << endl; };
~Derived() {
cout << "output from the destructor of class Derived" << endl;
}
void DoSomething() {
cout << "Do something in class Derived" << endl;
}
};
int main() {
Derived *pTese1 = new Derived();
pTese1->DoSomething();
delete pTese1;
cout <<"换行" <<endl;
base *pTest2 = new Derived();
pTest2->DoSomething();
delete pTest2;
return 0;
}
程序输出结果:
可以发现释放pTest2的时候并没有调用Derived的析构函数,但是如果在基类的析构函数处加上virtual声明之后就可以调用子类的析构函数。
1.14 拷贝构造函数是什么?什么是深拷贝和浅拷贝。
拷贝构造函数是一种特殊的构造函数,它由编译器调用用来完成一些基于同一类的其他对象的构件及初始化。
如果在类中没有显式地声明一个拷贝构造函数,那么,编译器会私下里制定一个函数来进行对象之间的位复制。这个隐含的拷贝构造函数简单地关联了所有的类成员。
默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数,c++不会再提供其他构造函数
在C++中,下面是3种对象需要复制的情况。因此,拷贝构造函数将会被调用。
- 一个对象以值传递的方式传入函数体。
- 一个对象以值传递的方式从函数返回。
- 一个对象需要通过另外一个对象进行初始化。
#include<iostream>
using namespace std;
class Test {
public:
int a;
Test(int x) {
a = x;
}
Test(const Test &test) {
cout << "copy constructor" << endl;
a = test.a;
}
};
//值传递到函数体里面 实参复制给形参
void fun1(Test test) {
cout << "fun1()..." << endl;
}
//值传递 从函数体返回
Test fun2() {
Test t(2);
cout << "fun2()..." << endl;
return t;
}
int main() {
Test t1(1);
Test t2 = t1;//用一个对象拷贝构造另一个对象
cout << "before fun1()..." << endl;
fun1(t1);
Test t3 = fun2();
cout << "after fun2()..." << endl;
return 0;
}
程序输出结果:
再来看看什么是深拷贝与浅拷贝
#include<iostream>
using namespace std;
#pragma warning(disable:4996)
class Test {
public:
char *buf;
Test() {
buf = NULL;
}
Test(const char *str) {
buf = new char[strlen(str) + 1];
strcpy(buf, str);
}
~Test() {
if (buf != NULL) {
delete buf;
buf = NULL;
}
}
};
int main() {
Test t1("Hello");
Test t2 = t1;
cout << "(t1.buf == t2.buf) ?" << (t1.buf == t2.buf ? "yes" : "no") << endl;
return 0;
return 0;
}
这段程序会发生崩溃,是由于Test t2=t1这行代码让t1、t2的buf指向了同一块内存,然后调用了两次析构函数析构了同一段内存导致了崩溃。这种就是浅拷贝。
我们可以在Test类里通过添加一个自定义的拷贝构造函数解决两次析构的问题。
#include<iostream>
using namespace std;
#pragma warning(disable:4996)
class Test {
public:
char *buf;
Test() {
buf = NULL;
}
Test(const char *str) {
buf = new char[strlen(str) + 1];
strcpy(buf, str);
}
Test(const Test &test) {
buf = new char[strlen(test.buf) + 1];
strcpy(buf, test.buf);
}
~Test() {
if (buf != NULL) {
delete buf;
buf = NULL;
}
}
};
int main() {
Test t1("Hello");
Test t2 = t1;
cout << "(t1.buf == t2.buf) ?" << (t1.buf == t2.buf ? "yes" : "no") << endl;
return 0;
return 0;
}
程序输出结果:
总结深拷贝与浅拷贝:
如果拷贝的对象中引用了某个外部的内容(例如分配在堆上的数据),那么在拷贝主峰对象的时候,让新旧两个对象指向同一个外部的内容,就是浅拷贝。如果在拷贝的这个对象的时候为新对象制造了外部对象的独立拷贝,那么就是深拷贝。
答案:
- 拷贝构造函数是一种特殊的构造函数,它有编译器调用完成一些基于同一类的其他对象的构件 及初始化。
- 浅拷贝是指让新旧两个对象指向同一个外部的内容,而深拷贝是指为新对象制作了外部对象独立拷贝。
1.15 写一个继承类的拷贝构造函数
#include<iostream>
using namespace std;
class Base {
public:
Base() :i(0) { cout << "Base()" << endl; } //默认普通构造函数
Base(int n) :i(n) { cout << "Base(int)" << endl; }//普通构造函数
//复制构造函数
Base(const Base&b) :i(b.i) {
cout << "Base(Base &)" << endl;
}
private:
int i;
};
class Derived :public Base {
public:
Derived() :Base(0), j(0) { cout << "Derived()" << endl; } //默认普通构造函数
Derived(int m, int n) :Base(m), j(n) { cout << "Derived(int)" << endl; } //普通构造函数
Derived(Derived& obj) :Base(obj), j(obj.j) {
cout << "Derived(Derived &)" << endl;
}
private:
int j;
};
int main() {
Base b(1);
Derived obj(2, 3);
cout << "------------------" << endl;
Derived d(obj);
cout << "---------------------" << endl;
return 0;
return 0;
}
程序输出结果:
Derived类继承自Base类,因此在Dervied类内不能使用obj.i或Base::i的方式访问Base的私有成员i。很明显,其复制构造函数只有使用Base(obj)的方式调用其基类的拷贝构造函数来给基类的私有成员i初始化。
1.16 拷贝构造函数与赋值函数有什么区别
- 拷贝构造函数是一个对象来初始化一块内存区域,这块内存就是新对象的内存区。
//拷贝构造函数
class A;
A a;
A b=a;
A b(a);
//赋值函数
class A;
A a;
A b;
b=a;
- 一般来说在数据成员包含指针对象的时候,应付两种不同的处理需求,一种是复制指针对象,一种是引用指针对象。拷贝构造函数在多数情况下是复制,赋值函数则是引用对象。
- 实现不一样。拷贝构造函数首先是一个构造函数,它调用的时候是通过参数传递来初始化一个对象。赋值函数则是把一个对象赋值给一个原有的对象,所以,如果原来的对象中有内存分配,要先把内存释放掉,而且还要检查一个两个对象是不是同一个对象,如果是就不需要任何操作。
1.17 了解C++类各成员函数的关系
#include<iostream>
using namespace std;
class A {
private:
int num;
public:
A() {
cout << "Default constructor" << endl;
}
~A() {
cout << "Desconstructor" << endl;
cout << num << endl;
}
A(const A &a) {
cout << "Copy constructor" << endl;
}
void operator =(const A &a) {
cout << "overload operator " << endl;
}
void SetNum(int n) {
num = n;
}
};
int main() {
A a1;
A a2(a1);
cout << "-------------" << endl;
A a3 = a1;//这里调用的是拷贝构造函数而不是赋值构造函数
A a5;
a5 = a1;//这里是赋值构造函数,赋值构造函数是对一个已经初始化的对象再赋值
cout << "-------------" << endl;
A &a4 = a1;//a4是a1的引用,不调用构造函数与析构函数
a1.SetNum(1);
a2.SetNum(2);
a3.SetNum(3);
a5.SetNum(5);
a4.SetNum(1);
return 0;
}
程序输出结果:
析构函数与构造函数顺序相反。
1.18 C++类的临时对象
#include<iostream>
using namespace std;
class B {
public:
B() {
cout << "default constructor" << endl;
}
~B() {
cout << "destructed" << endl;
}
B(int i) :data(i) {
cout << "constructed by parameter" << data << endl;
}
private:
int data;
};
B play(B b) {
return b;
}
对于不同的main函数
第一个:
int main(){
B t1 = play(5);
B t2 = play(t1);
return 0;
}
输出结果为:
首先传入到play函数生成一个临时对象b,然后函数结束调用b的析构函数,接下来ti传入play函数生成临时对象调用析构函数,然后main函数结束调用t2,t1的析构函数。t1传入play函数调用的是拷贝构造函数。
第二个:
int main(){
B t1 = play(5);
B t2 = play(10);
return 0;
}
输出结果:
首先paly(5)调用带参的构造函数,然后函数结束调用析构函数,play同样的情况,然后是t2,t1的析构函数。
1.19 函数返回一个对象时,产生一个临时的匿名对象
#include<iostream>
using namespace std;
class B {
public:
B() {
cout << "default constructor" << endl;
}
~B() {
cout << "destructed,我的地址是" << this << endl;
}
B(int i) :data(i) {
cout << "constructed by parameter" << data << endl;
}
private:
int data;
};
B play(B b) {
return b;
}
int main(){
B t1;
t1 = play(5);
return 0;
}
输出结果为:
从结果可以看到构造函数与析构函数个数不是匹配的,原因在于函数返回一个对象的时候并不是返回原对象,而是生成一个临时的匿名对象,由拷贝构造函数生成,是从函数体里面的临时对象拷贝至返回的匿名对象,所以另一个构造函数是拷贝构造,而且我们从析构函数打印的对象地址看出的确是三个对象,而且t1的地址与返回的匿名对象地址是一致的,而且不会调用拷贝构造函数。或者返回的对象是临时匿名对象的引用也不会调用析构函数。
但是如果是调用赋值构造函数去给h2赋值则函数返回的临时匿名对象对调用析构函数。见 1.20
程序作以下修改:
#include<iostream>
using namespace std;
class B {
public:
B() {
cout << "default constructor" <<"地址是:"<<this<< endl;
}
~B() {
cout << "destructed,我的地址是" << this << endl;
}
B(int i) :data(i) {
cout << "constructed by parameter" <<"地址是:" << this << endl;
}
B(const B &b) {
cout << "拷贝构造函数" <<"地址是:" << this << endl;
}
private:
int data;
};
B play(B b) {
cout << "形参对象的地址是" << &b << endl;
return b;
}
//这样修改之后不管是如果构造t1,都不会调用临时匿名对象的析构函数
/*
B& play(B b) {
cout << "形参对象的地址是" << &b << endl;
return b;
}
*/
int main(){
B t1;
t1 = play(5);
return 0;
}
输出结果为:
输出的结果与上述分析一致。
1.20 C++静态成员和临时对象
下面的程序又复习了上述的知识点。
#include<iostream>
using namespace std;
class human {
public:
static int human_num;
human(){
cout << "普通构造函数" << "地址是" << this << endl;
human_num++;
}
~human(){
human_num--;
cout << "析构的对象地址是:" << this ;
print();
}
human(const human & h) {
cout << "拷贝构造函数地址是" << this << "拷贝于" << &h << endl;
}
void print() {
cout << "调用print的是" << this << " " << " ";
cout << "human_num is " << human_num << endl;
}
};
int human::human_num = 0;
human fun(human x) {
cout << "通过fun调用print的是" << &x << " " << " ";
x.print();
return x;
}
int main() {
human h1;
h1.print();
human h2 = fun(h1);
h2.print();
cout << "--------------" << endl;
cout << "h1的地址是" << &h1 << endl;
cout << "h2的地址是:" << &h2 << endl;
return 0;
}
输出结果为:
如果把构造h2的方式改为调用赋值构造函数,则临时匿名对象会调用析构函数。
#include<iostream>
using namespace std;
class human {
public:
static int human_num;
human(){
cout << "普通构造函数" << "地址是" << this << endl;
human_num++;
}
~human(){
human_num--;
cout << "析构的对象地址是:" << this ;
print();
}
human(const human & h) {
cout << "拷贝构造函数地址是" << this << "拷贝于" << &h << endl;
}
void print() {
cout << "调用print的是" << this << " " << " ";
cout << "human_num is " << human_num << endl;
}
};
int human::human_num = 0;
human fun(human x) {
cout << "通过fun调用print的是" << &x << " " << " ";
x.print();
return x;
}
int main() {
human h1;
h1.print();
//注意这里的区别
human h2;
h2 = fun(h1);
h2.print();
cout << "--------------" << endl;
cout << "h1的地址是" << &h1 << endl;
cout << "h2的地址是:" << &h2 << endl;
return 0;
}
输出结果:
1.21 为什么C语言不支持函数重载而C++支持
- 函数重载是用来描述同名函数具有相同或者相似的功能,但数据类型或者是参数不同的函数管理操作。
- 函数名经过C++编译器处理后包含了原函数名、函数参数数量及返回类型信息,而C语言不会对函数名进行处理。
比如两个函数的定义是
- int add(int,int)
- float add(float,float)
C++ 对两个函数的处理是 _int_add_int_int float_add_float_float,这样的名字包含了函数名、函数参数数量及返回类型信息,C++ 就是靠这种机制来实现函数重载。
1.22 函数重载的正确声明
函数重载的规则:
- 函数名称必须相同。
- 参数列表必须不同(个数不同、类型不同、参数排列顺序不同等)。
- 函数的返回类型可以相同也可以不相同。
- 仅仅返回类型不同不足以成为函数的重载。
参考函数重载
2. 继承
2.1 C++类继承的三种关系
C++中主要有三种关系: public, protected, private
一个派生类可以同时有多个基类,这种情况称为多重继承,派生类只有一个
基类, 称为单继承。
-
public 继承
public继承时一种接口继承,子类可以代替父类完成父类接口所声明的行为。此时子类可以自动转换称为父类的接口,完成接口转换。从语法角度上来说,public继承会保留父类中成员(包括函数和变量)的可见性不变,也就是说,如果父类中的某个函数是public的,那么在被子类继承后仍然是public的。 -
protected 继承
protected 继承是一种实现继承,子类不能代替父类完成父类接口所声明的行为,此时子类不能自动转换成为父类的接口。从语法角度上来说,protected继承会将父类中的public可见性的成员修改成为protected可见性,相当于在子类中引入了protected成员,这样在子类中同样还是可以调用父类的protected和public成员,子类的子类也就可以调用被protected继承的父类的protected和public成员。 -
private 继承
private继承是一种实现继承,子类不能代替父类完成父类接口所声明的行为,此时子类不能自动转换成为父类的接口。从语法角度上来说,private继承会将父类中的public和protected可见性的成员修改成为private可见性。这样一来,虽然子类中同样还是可以调用父类的protected和public成员,但是子类的子类就不可以再调用被private继承的父类的成员了。
2.2 类型兼容原则
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类
的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之
外的所有成员。 这样,公有派生类实际就具备了基类的所有功能,凡是基类能
解决的问题,公有派生类都可以解决。
类型兼容规则中所指的替代包括以下情况:
- 子类对象可以当作父类对象使用
- 子类对象可以直接赋值给父类对象
- 子类对象可以直接初始化父类对象
- 父类指针可以直接指向子类对象
- 父类引用可以直接引用子类对象
在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继
承的成员。
2.3 继承中构造析构调用原则
- 在子类对象构造时,需要调用父类构造函数对其继承得来的成员进行初
始化。 - 在子类对象析构时,需要调用父类析构函数对其继承得来的成员进行清
理。
1、子类对象在创建时会首先调用父类的构造函数
2、父类构造函数执行结束后,执行子类的构造函数
3、当父类的构造函数有参数时,需要在子类的初始化列表中显示调用
4、析构函数调用的先后顺序与构造函数相反
见下面的程序:
#include<iostream>
using namespace std;
class object {
public:
object(const char *s) {
cout << "object()" << " " << s << endl;
}
~object() {
cout << " ~object()" << endl;
}
};
class parent:public object {
public:
parent(const char *s) : object(s) {
cout << "parent" << ' ' << s << endl;
}
~parent() {
cout << "~parent" << endl;
}
};
class child :public parent {
public:
child() :o2("o2"), o1("o1"), parent("parameter fron child") {
cout << "child()" << endl;
}
~child() {
cout << "~child" << endl;
}
private:
object o1;
object o2;
};
void run() {
child Child;
}
int main() {
run();
return 0;
}
输出结果:
在执行run函数的时候,首先创建了一个child对象,这时候首先调用父类的父类的构造,然后是父类的构造,最后才是自己的构造。析构的顺序与构造的顺序相反。
2.4 多继承
一个类有多个直接基类的继承关系称为多继承
2.5 虚继承
- 如果一个派生类从多个基类派生,而这些基类又有一个共同
的基类,则在对该基类中声明的名字进行访问时,可能产生二义性 - 如果在多条继承路径上有一个公共的基类,那么在继承路径的某处
汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象 - 要使这个公共基类在派生类中只产生一个子对象,必须对这个基类
声明为虚继承,使这个基类成为虚基类。 - 虚继承声明使用关键字 virtual
#include<iostream>
using namespace std;
class B {
public:
int b;
};
class B1 : public B {
private:
int b1;
};
class B2 : public B {
private:
int b2;
};
class C :public B1, public B2 {
private:
float d;
};
int main() {
C cc;
cc.B1::b = 0;
cout << cc.B1::b << endl;
//cout << cc.B2::b << endl;//error 使用了未初始化的变量
cc.B2::b = 1;
cout << endl;
cout << cc.B1::b << endl;
cout << cc.B2::b << endl;
return 0;
}
输出结果:
可以看到如果不是虚继承(class B2 : virtual public B),那么从同一个基类B继承过来的成员b,到最后是两个不同的成员,而如果使用了虚继承,最后两个继承路径指向的成员b是同一个,看下面的程序。
#include<iostream>
using namespace std;
class B {
public:
int b;
};
class B1 : virtual public B {
private:
int b1;
};
class B2 : virtual public B {
private:
int b2;
};
class C :public B1, public B2 {
private:
float d;
};
int main() {
C cc;
cc.B1::b = 0;
cout << cc.B1::b << endl;
cout << cc.B2::b << endl;
cc.B2::b = 1;
cout << endl;
cout << cc.B1::b << endl;
cout << cc.B2::b << endl;
return 0;
}
输出结果:
2.6 多继承的构造函数顺序
- 任何虚基类的构造函数按照它们被继承的顺序构造
- 任何非虚基类的构造函数按照它们被构造的顺序构造
- 任何成员对象的构造按照它们声明的顺序调用
- 类自身的构造函数
3. 多态
3.1 什么是多态
多态性的定义:统一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
- 编译时的多态性。编译时的多态性是指通过重载来实现的。对于非虚的成员来说,系统在编译时,根据传递的参数、返回的类型等信息决定实现何种操作。
- 运行时的多态性。运行时的多态性就是指直到系统运行时,才根据实际情况决定实现何种操作。C++中,运行时的多态性通过虚成员实现。
#include<iostream>
using namespace std;
class person {
public:
virtual void print() {
cout << "i from person" << endl;
}
};
class Chinese : public person {
public:
//可以加前缀 virtual 也可以不加
void print() {
cout << "i from chinese" << endl;
}
};
class American : public person {
public:
void print() {
cout << "i from american" << endl;
}
};
void printPerson(person &Person) {
Person.print();
}
int main() {
person p;
Chinese c;
American a;
printPerson(p);
printPerson(c);
printPerson(a);
return 0;
}
输出结果:
3.2 构造函数调用虚函数
#include<iostream>
using namespace std;
class A {
public:
A() {
dosth();
}
virtual void dosth() {
cout << "i form a" << endl;
}
};
class B :public A {
public:
/*B() {
dosth();
}*/
virtual void dosth() {
cout << "i from b" << endl;
}
};
int main() {
B b;
return 0;
}
输出结果:
3.3 为什么要引入抽象基类和虚函数
纯虚函数在基类中没有定义的,必须在子类中加以实现,很像Java中的接口函数。如果基类含有一个或多个纯虚函数,那么它就属于抽象基类,不能实例化对象。
为什么要引入抽象基类和纯虚函数,有以下两点。
- 为方便使用多态性。
- 在很多情况下,基类本身生产对象是不合情理的。例如,动物作为一个基类可以派生出老虎、狮子等子类,但动物本身生产对象明显不合常理。抽象基类不能够被实例化,它定义的纯虚函数相当于接口,能把派生类的共同行为提取出来。
3.4 赋值兼容(多态实现的前提)
赋值兼容规则是指在需要基类对象的任何地方都可以使用公有派生类的对象来替代。
赋值兼容是一种默认行为,不需要任何的显示的转化步骤。
赋值兼容规则中所指的替代包括以下的情况:
- 派生类的对象可以赋值给基类对象。
- 派生类的对象可以初始化基类的引用。
- 派生类对象的地址可以赋给指向基类的指针。
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。
#include<iostream>
using namespace std;
class parent {
public:
parent(int a) {
this->a = a;
cout << "parent a" << a << endl;
}
//virtual void print() {
// cout << "parent 打印 a" << a << endl;
//}
void print() {
cout << "parent 打印 a" << a << endl;
}
private:
int a;
};
class child :public parent {
public:
child(int b) :parent(10) {
this->b = b;
cout << "child b" << b << endl;
}
void print() {
cout << "child 打印 b" << endl;
}
//virtual void print() {
// cout << "child 打印 b" << endl;
//}
private:
int b;
};
void howToprint(parent *base) {
base->print();
}
void howToPrint2(parent &base) {
base.print();
}
int main() {
parent *base = NULL;
parent p1(20);
child c1(30);
base = &p1;
base->print();
base = &c1;
base->print();
parent &base2 = p1;
base2.print();
parent &base3 = c1;
base3.print();
howToprint(&p1);
howToprint(&c1);
howToPrint2(p1);
howToPrint2(c1);
return 0;
}
输出结果:
3.5 静态联编和动态联编
- 联编是指一个程序模块、代码之间互相关联的过程。
- 静态联编,是程序的匹配,连接在编译阶段实现,也称为早起匹配。重载函数使用静态联编。
- 动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟邦定)。switch和if语句是动态联编的例子。
3.6 重载和覆写有什么区别
重载是指子类改写了父类的方法,覆写是指同一个函数的不同版本之间参数不同。
重载是编写一个与已有函数同名但是参数列表不同的方法,它具有如下所示的特征。
- 方法名必须相同。
- 参数列表必须不相同,与参数列表的顺序无关。
- 返回值类型可以不相同。
覆写是派生类重写基类的虚函数,它具有如下所示的特征。
- 只有虚方法和抽象方法才能够被覆写。
- 相同的函数名。
- 相同的参数列表。
- 相同的返回值类型。
重载是一种语法规则,由编译器在编译阶段完成,不属于面向对象的编程;而覆写是由运行阶段决定的,是面向对象编程的重要特征。
3.7 虚析构函数
- 构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数。
- 析构函数可以是虚的。虚析构函数用于delete运算符正确析构动态对象。
参看这篇博客,说的很清楚。
虚析构函数
3.8 虚函数表和vptr指针
简单的说,虚函数是通过虚函数表实现的,引出问题,什么是虚函数表。
事实上,如果一个类中含有虚函数,则系统会为这类分配一个指针成员指向一张虚函数表,表中每一项指向一个虚函数的地址,实现上就是一个函数指针的数组。
说明:
- 通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需
要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就
确定了调用的函数。在效率上,虚函数的效率要低很多。 - 出于效率考虑,没有必要将所有成员函数都声明为虚函数。
- C++编译器,执行run函数,不需要区分是子类对象还是父类对象,而是
直接通过p的VPTR指针所指向的对象函数执行即可。
#include<iostream>
using namespace std;
class parent1 {
public:
parent1(int a = 0) {
this->a = a;
}
void print() {
cout << "parent" << endl;
}
private:
int a;
};
class parent2 {
public:
parent2(int a = 0) {
this->a = a;
}
virtual void print() {
cout << "parent" << endl;
}
private:
int a;
};
int main() {
cout << "sizeof(parent1)" << sizeof(parent1) << endl;
cout << "sizeof(parent2)" << sizeof(parent2) << endl;
return 0;
}
输出结果:
parent2相比parent1多了一个指针。
3.9 对多态的总结
- 多态的实现效果
多态:同样的调用语句有多种不同的表现形态。 - 多态实现的三个条件
有继承、有virtual重写、有父类指针(引用)指向子类对象。 - 多态的C++实现
virtual关键字,告诉编译器这个函数要支持多态;不是根据指针类型判断如何调用;而是要根据指针所指向的实际对象类型来判断如何调用。
多态的理论基础
动态联编PK静态联编。根据实际的对象类型来判断重写函数的调用。 - 多态的重要意义
设计模式的基础,是框架的基石。 - 多态原理探究
- 虚函数表和vptr指针。
3.10 纯虚函数和抽象类
首先说明一下纯虚函数和虚函数的区别
- 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖。这样编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留在子类里去实现。
- 虚函数在子类里面也可以不重载,但纯虚函数必须在子类去实现(如果这个子类需要去实例化对象的话,如果不需要实例化对象,那么这个子类仍然是一个抽象类),这就像Java的接口一样。通常把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因此很难预料到父类里面的这个函数不在子类里面不去修改它的实现。
- 虚函数的类用于“实作继承”,也就是说继承接口的同时也继承了父类的实现。当然,大家也可以完成自己的实现。纯虚函数的类用于“介面继承”,即纯虚函数关注的是接口的统一性,实现由子类完成。
- 带纯虚函数的类叫虚基类,这种基类不能直接生成对象,而只有被继承,并重写其函数后,才能使用。这样的类也叫抽象类。
3.11 抽象类杂多继承中的应用
绝大多数面向对象语言都不支持多继承,绝大多数面向对象语言都支持接口的概念
C++ 中没有接口的概念,C++中可以使用纯虚函数实现接口。
接口类中只有函数原型定义,没有任何数据的定义
#include<iostream>
using namespace std;
class Interface1 {
public:
virtual void print() = 0;
virtual int add(int a, int b) = 0;
};
class Interface2 {
public:
virtual void print() = 0;
virtual int add(int a, int b) = 0;
virtual int sub(int a, int b) = 0;
};
class parent {
public:
int a;
};
class Child :public parent, public Interface1, public Interface2 {
public:
void print() {
cout << "Child::print" << endl;
}
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
};
int main() {
Child c;
c.print();
cout << c.add(3, 5) << endl;
cout << c.sub(10, 5) << endl;
Interface1 *i1 = &c;
Interface2 *i2 = &c;
cout << i1->add(7, 8) << endl;
cout << i2->add(7, 8) << endl;
return 0;
}
输出结果:
3.12 虚函数可以是内联的吗
不可以。
参考
4.泛型编程
4.1 函数模板与类模板分别是什么
函数模板是一种抽象的函数定义,它代表一类同构函数。通过用户提供的具体参数,C++ 编译器在编译时刻能够将函数模板实例化,根据同一个模板创建出不同的具体函数。这些函数之间的不同之处主要在于函数内部一些数据类型的不同,而由模板创建的函数的使用方法与一般的使用方法相同。
C++ 提供的类模板是一种更高层次的抽象的类定义,用于使用相同代码创建不同的类模板的定义与函数模板的定义类似,只是把函数模板中的函数定义部分换作类说明,并对类的成员函数进行定义即可。
答案:
- 函数模板是一种抽象的函数定义,它代表一类同构函数。类模板是一种更高层次的抽象的类定义。
- 函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显示地指定。(所以函数在调用的时候可以不需要显示的指定类型)
4.2 函数模板与函数重载
#include<iostream>
using namespace std;
void myswap(int &a, int &b) {
int t = a;
a = b;
b = t;
}
void myswap(char &a, char &b) {
char t = a;
a = b;
b = t;
}
int main() {
int x = 1;
int y = 2;
myswap(x, y);
cout << "x" << x << "y" << y << endl;
char a = 'c';
char b = 'b';
myswap(a, b);
cout << "a:" << a << "b:" << b << endl;
return 0;
}
输出结果:
模板形式:
#include<iostream>
using namespace std;
template<typename T>
void myswap(T &a, T &b) {
T t = a;
a = b;
b = t;
}
int main() {
int x = 1;
int y = 2;
myswap<int>(x, y);
cout << "a:" << x << "b" << y << endl;
char a = 'a';
char b = 'b';
myswap<char>(a, b);
cout << "a:" << a << "b:" << b << endl;
return 0;
}
输出结果:
4.3 编译器的编译过程
编译命令: g++ hello.c -o hello
template.cpp
#include<iostream>
using namespace std;
template<typename T>
void myswap(T &a, T &b) {
T t = a;
a = b;
b = t;
}
int main() {
int x = 1;
int y = 2;
//myswap<int>(x, y);
myswap<>(x, y);
cout << "a:" << x << "b" << y << endl;
char a = 'a';
char b = 'b';
myswap<char>(a, b);
cout << "a:" << a << "b:" << b << endl;
return 0;
}
输入命令:
g++ template.cpp -o template
然后就会生产一个template的可执行文件
4.4 类模板
类模板与函数模板类似,仅是数据类型进行了泛化。
#include<iostream>
using namespace std;
template<typename T>
void myswap(T &a, T &b) {
T t = a;
a = b;
b = t;
}
template <typename T>
class A {
public:
A(T t) {
this->t = t;
}
virtual T &getT() {
return t;
}
protected:
T t;
};
//模板类派生普通类 ,需要指明模板类的类型
class B :public A<int> {
public:
B(int a, int b) : A<int>(a) {
this->b = b;
}
void printB() {
cout << "b:" << b << endl;
}
private:
int b;
};
//模板类派生模板子类
template <typename T>
class C :public A<T> {
public:
C(T a, T c) :A<T>(a) {
this->c = c;
}
void printc() {
cout << "c" << c << endl;
}
virtual T &getT() {
return c;
}
private:
int c;
};
int main() {
A<int> a(100);
cout << a.getT() << endl;
C<int> c(10, 20);
cout << c.getT() << endl;
A<int> *p;
p = &c;
cout << p->getT() << endl;
cout << "-----------" << endl;
A<int> aa(1000);
cout << aa.getT() << endl;
aa = c;
//可以用子类去赋值给基类,但是被赋值的基类只能调用自身的成员
cout << aa.getT() << endl;
return 0;
}
输出结果:
4.5 函数体写在类中
#include<iostream>
using namespace std;
template <class T>
class Complex {
friend ostream & operator << (ostream & os, Complex &c) {
os << "(" << c.a << "+" << c.b << "i" << ")";
return os;
}
public:
Complex(){}
Complex(T a, T b) {
this->a = a;
this->b = b;
}
void printComplex() {
cout << "(" << a << "+" << b << "i" << ")" << endl;
}
Complex operator +(Complex &anther) {
Complex temp(a + anther.a, b + anther.b);
return temp;
}
private:
T a;
T b;
};
int main() {
Complex<int> a(10, 20);
Complex<int> b(3, 4);
Complex<int> c;
c = a + b;
c.printComplex();
cout << c << endl;
return 0;
}
输出结果:
4.6 函数体写在类外
#include<iostream>
using namespace std;
template <class T>
class Complex;
template <class T>
Complex<T> MyuSub(Complex <T>&one, Complex<T> &another);
template<class T>
class Complex {
public:
//友元需要在 operator << 与参数列表之间加上 <T>
friend ostream & operator << <T> (ostream &os, Complex<T> & another);
//如果友元不是 << >> 需要在类的前面加一个声明
friend Complex<T> MyuSub <T> (Complex<T> &one, Complex<T> &another);
Complex();
Complex(T a, T b);
Complex operator + (Complex &another);
Complex operator - (Complex &another);
void printComplex();
private:
T a;
T b;
};
template <class T>
Complex<T>::Complex() {
}
template<class T>
Complex<T>::Complex(T a, T b) {
this->a = a;
this->b = b;
}
template<class T>
Complex<T> Complex<T>::operator+(Complex<T> &another) {
Complex temp(a + another.a, b + another.b);
return temp;
}
template <class T>
Complex<T> Complex<T>::operator-(Complex &another) {
Complex<T> temp(this->a - another->a, this->b - another->b);
return temp;
}
template<class T>
void Complex<T>::printComplex() {
cout << "(" << a << "+" << b << "i" << ")" << endl;
}
template<class T>
ostream & operator << (ostream &os, Complex<T> & another) {
os << "(" << another.a << "+" << another.b << "i" << ")";
return os;
}
template <class T>
Complex<T> MyuSub(Complex<T>& one, Complex<T>& another)
{
Complex<T> temp(one.a - another.a, one.b - another.b);
return temp;
}
int main() {
Complex<int> a(10, 20);
Complex<int> b(3, 4);
a.printComplex();
Complex<int> c;
c = a + b;
c.printComplex();
cout << c << endl;
c = MyuSub(a, b);
cout << c << endl;
return 0;
}
输出结果:
4.7 类模板中的static
#include<iostream>
using namespace std;
template <class T>
class A {
public:
static T s_vale;
};
template <class T>
T A<T>::s_vale = 0;
int main() {
A<int> a1, a2, a3;
A<char> b1, b2, b3;
a1.s_vale = 10;
b1.s_vale = 'a';
cout << a1.s_vale << endl;
cout << b1.s_vale << endl;
a1.s_vale++;
cout << a2.s_vale << endl;
cout << a3.s_vale << endl;
b1.s_vale++;
cout << b2.s_vale << endl;
cout << b3.s_vale << endl;
return 0;
}
输出结果:
5. STL(标准模板库)
5.1 什么是STL
STL(Standard Template Libary),即标准模板库,它涵盖了常用的数据结构和算法,并且具有跨平台的特点。将泛型编程思想和STL库用于系统设计中,明显降低了开发强度,提高了程序的可维护性及代码的可重用性。
STL是C++ 标准库的一部分。STL的基本观念就是把数据和操作分离。
STL中数据由容器类别来加以管理,操作则由可定制的算法来完成。迭代器在容器和算法之间充当粘合剂,它使得任何算法都可以和任何容器进行交互运作。STL含有容器、算法、迭代器组件。
其中STL序列容器:vector、string、deque和list。
STL关联容器:set、multiset、map和multimap、unordered_map、ordered_set。
STL适配容器:stack、queue、和priority_queue。
5.2 智能指针
参考小贺c++八股文 公众号 : herongwei
1、智能指针详解
静态内存用来保存局部static对象以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非statci对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;statci对象在使用之前分配,在程序结束时销毁。
除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间或堆。程序用堆来存储动态分配的对象,即那些在程序运行时分配的对象,动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们代码必须显式地销毁他们。
为了更容易的使用动态内存,新标准库提供了两种智能指针类型来管理动态对象。
2、auto_ptr
auto_ptr是c++98标准下的,现在已被弃用。
许多数据重要的结构以及应用,例如链表、STL容器、串、数据库系统以及交互式应用必须使用动态内存分配,因此仍然冒着玩意发生异常导致内存溢出的风险。C++ 标准化委员会意识到了这个漏洞并在标准库中添加了一个特殊的类模板,它就是std::auto_ptr,其目的是促使动态内存和异常之前进行平滑的交互。auto_ptr保证当异常掷出时,分配的对象能被自动销毁,内存能被自动释放。下面是auto_ptr的用法。
#include<memory>
#include<iostream>
#include<string>
using namespace std;
int main() {
auto_ptr<string> p1(new string{ "hello" });
auto_ptr<string> p2;
p2 = p1;
//p1管理的内容被p2夺走,用p1去访问出错
//cout << *p1 << endl;//erro
return 0;
}
#include<iostream>
#include<memory>
#include<string>
using namespace std;
class Test {
public:
Test() { name = NULL; };
Test(const char* strname) {
name = new char[strlen(strname) + 1];
strcpy(name, strname);
}
Test& operator = (const char *strname) {
if (name != NULL) {
delete name;
}
name = new char[strlen(strname) + 1];
strcpy(name, strname);
return *this;
}
void ShowName() { cout << name << endl; }
~Test() {
if (name != nullptr) {
delete name;
}
name = nullptr;
cout << "delete name" << endl;
}
private:
char *name;
};
int main() {
auto_ptr<Test> Testptr(new Test("Terry"));
//auto_ptr<Test> Testptr1 = new Test("Terrry1");//不能隐式转换 auto_ptr的构造函数是explicit的
Testptr->ShowName();
*Testptr = "David";
Testptr->ShowName();
int y = 1;
int x = 0;
y = y / x;//产生异常 但是auto_ptr的内容依然可以被释放
return 0;
}
3、shard_prt,unique_ptr,weak_ptr
参考《c++ prime 第五版》
我们知道除了静态内存和栈内存外,每个程序还有一个内存池,这部分内存被称为自由空间或者堆。程序用堆来存储动态分配的对象即那些在程序运行时分配的对象,当动态对象不再使用时,我们的代码必须显式的销毁它们。
在C++中,动态内存的管理是用一对运算符完成的:new和delete,new:在动态内存中为对象分配一块空间并返回一个指向该对象的指针,delete:指向一个动态独享的指针,销毁对象,并释放与之关联的内存。
//由于new返回的指针是指向该对象的指针 所以我们可以使用auto
auto p = new Test();
动态内存管理经常会出现两种问题:一种是忘记释放内存,会造成内存泄漏;一种是尚有指针引用内存的情况下就释放了它,就会产生引用非法内存的指针。
为了更加容易(更加安全)的使用动态内存,引入了智能指针的概念。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。标准库提供的两种智能指针的区别在于管理底层指针的方法不同。
- shared_ptr允许多个指针指向同一个对象
- unique_ptr则“独占”所指向的对象,替代auto_ptr
- 标准库还定义了一种名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。
这三种智能指针都定义在memory头文件中。
智能指针操作:
1. shared_ptr 类
shared_ptr实现共享式拥有概念,多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁时”时候释放。从名字shared就可以看出了资源可以被多个指针共享,它使用计数机制表面资源被几个指针共享。
可以通过成员函数use_count()来查看资源的所有者个数,除了科研通过new构造,还可以通过传入auto_ptr,unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
shared_ptr是为了解决auto_ptr在对象所有权上的局限性,在使用引用计数的机制上提供了科研共享所有权的智能指针。
创建智能指针时必须提供额外的信息,指针可以指向的类型。
shared_ptr<stirng> p1;
shared_ptr<list<int>> p2;
make_shared函数:
最安全的分配和使用动态内存的方法就是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
shared_ptr<string> p1;
shared_ptr<int> p2;
p1 = make_shared<string>(10, '9');
p2 = make_shared<int>(999);
cout << "p1->val: " << *p1 << endl;
cout << "p2->val: " << *p2 << endl;
输出结果:
如果在make_shared函数中不传递参数,则会进行值初始化。
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数,无论何时我们拷贝一个shared_ptr,计数器都会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减,一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。
shared_ptr<string> p1 = make_shared<string>(10,'9');
auto p3(p1);
cout << p3.use_count() << endl;
输出结果:
如果对定义初始化好的shared_ptr进行赋值,则原指向的对象指针个数-1,赋值中右值指向的对象指针个数+1,这很容易理解。
shared_ptr<int> p1;
shared_ptr<int> p2;
p1 = make_shared<int>(10);
p2 = make_shared<int>(999);
auto p3(p1);
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
p3 = p2;
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
如果指向对象的最后一个shared_ptr被销毁时,shared_ptr类会自动调用对象的析构函数,无需用户自己delete。
直接管理内存
C++定义了两个运算符来分配和释放动态内存,new和delete,使用这两个运算符非常容易出错。
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。
string *ps = new string;//初始化为空string
int *pi = new int;//pi指向一个未初始化的int
内存耗尽:
虽然现代计算机通常都配备大容量内村,但是自由空间被耗尽的情况还是有可能发生。一旦一个程序用光了它所有可用的空间,new表达式就会失败。默认情况下,如果new不能分配所需的内存空间,他会抛出一个bad_alloc的异常,我们可以改变使用new的方式来阻止它抛出异常
//如果分配失败,new返回一个空指针
int *p1 = new int;//如果分配失败,new抛出std::bad_alloc
int *p2 = new (nothrow)int;//如果分配失败,new返回一个空指针
使用new和delete管理动态内存常出现的问题:
- 忘记delete内存
- 使用已经释放的对象
- 同一块内存释放两次
在delete之后,指针就变成了空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的地址,为了安全,我们通常会对指针赋值为空nullptr。
shared_ptr 与 new结合使用
如果我们不初始化一个智能指针,它就会被初始化成一个空指针,接受指针参数的职能指针是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针,必须直接初始化形式来初始化一个智能指针。
//shared_ptr<int> p4 = new int(1024);//error 不存在int* 到 shared_ptr 的适当构造函数
shared_ptr<int> p5(new int(1024));//正确
如果混合使用的话,智能指针自动释放之后,普通指针有时就会变成悬空指针,当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。
也不要使用get初始化另一个智能指针或为智能指针赋值
智能指针陷阱:
(1)不使用相同的内置指针值初始化(或reset)多个智能指针。
(2)不delete get()返回的指针
(3)不使用get()初始化或reset另一个智能指针
(4)如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
(5)如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
2. unique_ptr
某个时刻只能有一个unique_ptr指向一个给定对象,由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。
#include<memory>
#include<iostream>
#include<string>
using namespace std;
int main() {
unique_ptr<string> p1(new string{ "hello" });
unique_ptr<string> p2;
//unique_ptr 要求独占 所以把p1的值赋值给p2会报错
//p2 = p1;
return 0;
}
虽然我们不能拷贝或者赋值unique_ptr,但是可以通过调用release或reset将指针所有权从一个(非const)unique_ptr转移给另一个unique
//将所有权从p1(指向string Stegosaurus)转移给p2
unique_ptr<string> p2(p1.release());//release将p1置为空
unique_ptr<string>p3(new string("Trex"));
//将所有权从p3转移到p2
p2.reset(p3.release());//reset释放了p2原来指向的内存
release成员返回unique_ptr当前保存的指针并将其置为空。因此,p2被初始化为p1原来保存的指针,而p1被置为空。
reset成员接受一个可选的指针参数,令unique_ptr重新指向给定的指针。
调用release会切断unique_ptr和它原来管理的的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。
不能拷贝unique_ptr有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr.最常见的例子是从函数返回一个unique_ptr.
unique_ptr<int> clone(int p)
{
//正确:从int*创建一个unique_ptr<int>
return unique_ptr<int>(new int(p));
}
3. weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock,此函数检查weak_ptr指向的对象是否存在。如果存在,lock返回一个指向共享对象的shared_ptr,如果不存在,lock将返回一个空指针
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么两个指针的引用计数永远不可能下降为0,也就是资源永远也不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
#include<iostream>
#include<memory>
#include<string>
using namespace std;
class Dog {
public:
Dog() {
cout << "Nameless dog created." << endl;
s1 = "nameless";
}
Dog(string name) {
cout << "Dog is created:" << name << endl;
s1 = name;
}
void makefriend(shared_ptr<Dog> f)
{
ptr_friend = f;
}
void foo() {
cout << "Dog " << s1 << " rules. " << endl;
}
~Dog() {
cout << "Dog is destoryed:" << s1 << endl;
}
private:
//shared_ptr<Dog> ptr_friend;
weak_ptr<Dog> ptr_friend;
string s1;
};
void test()
{
shared_ptr<Dog> p1 = make_shared<Dog>("dog1");
shared_ptr<Dog> p2 = make_shared<Dog>("dog2");
p1->makefriend(p2);
p2->makefriend(p1);
}
int main()
{
test();
return 0;
}
5.3 STL详细总结
5.3.1 概述
STL提供了六大组件,彼此之间可以组合套用,这六大组件分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器。
-
容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据,从实现角度来看,STL容器是一种class template。
-
算法:各种常用的算法,如sort、find、copy、for_each。从实现的角度来看,STL算法是一种function tempalte.
-
迭代器:扮演了容器与算法之间的胶合剂,共有五种类型,从实现角度来看,迭代器是一种将operator* , operator-> , operator++,operator–等指针相关操作予以重载的class template. 所有STL容器都附带有自己专属的迭代器,只有容器的设计者才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。
-
仿函数:行为类似函数,可作为算法的某种策略。从实现角度来看,仿函数是一种重载了operator()的class 或者class template
-
适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
-
空间配置器:负责空间的配置与管理。从实现角度看,配置器是一个实现了动态空间配置、空间管理、空间释放的class tempalte.
STL六大组件的交互关系,容器通过空间配置器取得数据存储空间,算法通过迭代器存储容器中的内容,仿函数可以协助算法完成不同的策略的变化,适配器可以修饰仿函数。
5.3.2 容器
STL容器就是将运用最广泛的一些数据结构实现出来。
常用的数据结构:数组(array) , 链表(list) , tree(树) ,栈(stack), 队列(queue), 集合(set),映射表(map), 根据数据在容器中的排列特性,这些数据分为 序列式容器 和 关联式容器两种。
序列式容器强调值的排序,序列式容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。Vector容器、Deque容器、List容器等。
关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。关联式容器另一个显著特点是:在值中选择一个值作为关键字key,这个关键字对值起到索引的作用,方便查找。Set/multiset容器 Map/multimap容器
容器 | 底层数据结构 | 时间复杂度 | 有无序 | 可不可重复 |
---|---|---|---|---|
array | 数组 | 随机读改O(1) | 无序 | 可重复 |
vector | 数组 | 随机读改、尾部插入、尾部删除O(1),头部插入、头部删除O(n) | 无序 | 可重复 |
deque | 双端队列 | 头尾插入,头尾删除O(1) | 无序 | 可重复 |
forward_list | 单向链表 | 插入,删除O(1) | 无序 | 可重复 |
list | 双向链表 | 插入,删除O(1) | 无序 | 可重复 |
stack | deque/list | 顶部插入,顶部删除O(1) | 无序 | 可重复 |
queue | deque/list | 尾部插入,头部删除O(1) | 无序 | 可重复 |
priority_queue | vector/max-heap | 插入、删除O(log2n) | 有序 | 可重复 |
5.3.3 算法
5.3.3.1 排序
sort() 可以排序字符串和数字,可以传入自定义比较函数指针。
谓词:
谓词是一个可调用的表达式,其返回结果是一个能用作条件的值。标准库算法所使用的谓词分为两类:一元谓词,二元谓词,区别在于接受的参数个数。
比如我么希望排序按照长度来比较,可以定义如下的函数。
bool cmp(const string &s1,const string &s2){
return s1.size() < s2.size();
}
sort函数调用的时候使用如下的格式:
sort(words.begin(),words.end(),cmp);
函数定义需要注意的几点:
- 如果方法函数是在类内定义的需要注意必须定义为静态成员函数,加上statci声明。
- 或者定义在类外。
- 使lambda表达式
sort(s.begin(),s.end(),[](const string &s1,const string &s2){return s1.size() < s2.size();});
lambda表示式需要注意的几点:
前面的[]并不是瞎写着玩的,是用来捕捉其局部变量,捕获列表指引lambda在其内部包含访问局部变量所需的信息。
如:
[sz](const string &a){ return a.size() >= sz; };
5.4 priority_queue
其它
1. CPP 输入
1.1 cin 遇到空格或者回车停止
在题目中如果遇到如下的输入
题目的意思是每一行是一个面试官掌握的语言,但是没有给每个面试官会的语言个数,这时候就需要我们对cin做一些细节上的操作
4
c++ java py
c++
java
python java
#include<iostream>
#include<vector>
#include<string>
using namespace std;
int main(){
string s;
int n;
cin>>n;
vector<vector<string>> res(n);
for(int i = 0; i < n; i++){
while(cin>>s){
res[i].push_back(s);
//遇到回车就跳出循环 读下一个面试官的输入
if(cin.get() == '\n') break;
}
}
for(auto c : res){
for(auto i : c){
cout<<i<<" ";
}
puts("");
}
return 0;
}
输出:
1.2 getline 遇到回车停止忽略空格
例如有如下输入
c++ java py c++ java python java
#include<iostream>
#include<vector>
#include<string>
using namespace std;
int main(){
string s;
int n;
getline(cin,s);
cout<<s;
return 0;
}
输出:
cin对输入流分隔
例如有如下输入
10.0.3.193
我们想要得到的是小数点分隔的两边数字。
可以这样处理。
int a,b,c,d;
char ch;
cin>>a>>ch>>b>>ch>>c>>ch>>d;
2. C/C++ 对齐原则
3. C++中overload,override,overwrite的区别详细解析
Overload(重载):在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
Override(覆盖):是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
Overwrite(重写):是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
4. new/delete和malloc/free有什么关系
int *p = new int[2];
int *q = (int*) malloc (2 * sizeof int);
- new 与 delete直接带具体类型的指针,malloc和free返回void类型的指针。
- new 类型是安全的,而malloc不是。例如int *p = new float[2]就会报错;而int p = malloc(2 * sizeof (int)) 编译时编译器就无法指出错误。
- new 一般分为两步:new操作个构造。new操作对应于malloc,但new操作可以重载,可以自定义内存分配策略,不做内存分配,甚至分配到非内存设备上,而malloc不行。
- new调用构造函数,malloc不能;delete调用析构函数,而free不能。
- malloc / feee需要库文件stdlib.h支持,new / delete不需要。
注意: delete和free被调用后,内存不会立即回收,指针也不会指向空,delete或free仅仅是高速操作系统,这一块内存被释放了,可以用作其他用途,但是由于没有重新对这块内存进行写操作,所以内存中的变量数值并没有发生变化,出现野指针的情况。因此,释放完内存后,应该将该指针指向NULL。
5. C++中内存分配情况
栈 : 由编译器管理分配个回收,存放局部变量和函数参数。
堆:由程序员管理,需要手动new malloc delete free进行分配和回收,空间较大,但可能会出现内存泄漏和空闲碎片的情况。
全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量和静态变量。
常量存储区:存储常量,一般不允许修改。
代码区:存储程序的二进制代码。
6. 指针和引用的区别
指针和引用都是一种内存地址的概念,指针是一个实体,引用只是一个别名。
7. static关键字有什么作用?
- 修饰局部变量时,使得该变量在静态存储区分配内存;只能在首次函数调用中进行首次初始化,之后的函数调用不再进行初始化;其生命周期与程序相同,但其作用域为局部作用域,并不能一直被访问。
- 修饰全局变量时,使得该变量在静态存储区分配内存;在声明该变量的整个文件中都是可见的,而在文件外是不可见的;
- 修饰函数时,在声明该函数的整个文件中都是可见的,而在文件外是不可见的,从而可以在多人协作时避免同名的函数冲突;
- 修饰成员变量时,所有的对象都只维持一份拷贝,可以实现不同对象间的数据共享;不需要实例化对象即可访问;不能在类内部初始化。一般在类外部初始化,并且初始化时不加static;
- 修饰成员函数时,该函数不接受this指针,只能访问类的静态成员;不需要实例化对象即可访问。
8. define 和 const 有什么区别?
- 编译器处理方式不同: #define 宏是在预处理阶段展开,不能对宏定义进行调试,而const常量是在编译阶段使用;
- 类型和安全检查不同;#define宏没有类型,不做任何类型检查,仅仅是代码展开,可能产生边际效应等错误,而const常量有具体类型,在编译阶段会执行类型检查;
- 存储方式不同,#define宏仅仅是代码展开,在多个地方进行字符替换,不会分配内存,存储与程序的代码段中,而const常量会分配内存,但只维持一份拷贝,存储与程序的数据段中。
- 定义域不同: #define宏不受定义域限制,而const常量只在定义域内有效。
9. 对于一个频繁使用的短小函数,应该使用什么来实现,有什么缺点?
应该使用inline内联函数,即编译器将inline内联函数内的代码替换到被调用的地方。
优点:
- 在内联函数被调用的地方进行代码展开,省去函数调用的时间,从而提高程序运行效率;
- 相比于宏函数,内联函数在代码展开时,编译器会进行语法安全检查或数据类型转换,使得更加安全;
缺点: - 代码膨胀,产生更多的开销;
- 如果内联函数内代码块的执行时间比调用时间长的多,那么效率的提升并没有那么大;
- 如果修改内联函数,那么所有调用该函数的代码文件都需要重新编译;
内联声明只是建议,是否内联由编译器决定,所有实际并不可控。
10. 悬挂指针与野指针有什么区别?
- 悬挂指针: 当指针所指向的对象被释放,但是该指针没有任何改变,以至于其仍然指向已经被回收的内存地址,这种情况下该指针被称为悬挂指针。
- 野指针: 未初始化的指针被称为野指针
11. 结构体可以直接赋值吗
声明时可以直接初始化,同一结构体的不同对象之间可以直接赋值,但是当结构体中含有指针成员时一定要小心。
12. sizeof 和 strlen的区别
- sizeof 是一个操作符 strlen是库函数
- sizeof 的参数可以是数据的类型也可以是变量,而strlen只能是以结尾为'\0'的字符串做参数。
- 编译器在编译时就计算出了sizeof 的结果,而strlen函数必须在运行时才能计算出来。并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。
- 数组做sizeof 的参数不退化,传递给strlen就退化为指针了。
评论区