卑微声明:一些题目答案是我从其他博主那看到,觉得讲的不错的,摘过来的。因为题目较多,没有一一把原博文链接贴出,如果侵权,请联系我。360度鞠躬。
渣女声明:一些代码和发言是我自己根据理解写的,错误概不负责啊,哈哈,如果你发现了错误,欢迎留言指正
设计模式:是解决软件开发某些特定问题而提出的一些解决方案也可以理解成解决问题的一些思路。通过设计模式可以帮助我们增强代码的可重用性、可扩充性、 可维护性、灵活性好。我们使用设计模式最终的目的是实现代码的 高内聚 和 低耦合。
设计模式主要分三个类型:
创建型:单例模式、抽象工厂、工厂方法、建造模式、原型模式等
结构型:外观模式、适配器模式、代理模式、装饰模式等
行为型:迭代器模式、观察者模式、命令模式等
单例模式的实现 更多参考
含义
一个类只有一个实例,提供一个访问它的全局访问点。其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;
运用场景
#include
using namespace std;
class singleton {
private:
singleton() {
cout << "我构造了啊" << endl;
};
singleton(singleton&) {
};
singleton& operator=(const singleton&) {
};
static singleton* instance_ptr;
public:
~singleton(){
cout << "我析构了啊" << endl;
};
static singleton* get_instance() {
if (instance_ptr == nullptr) {
instance_ptr = new singleton();
}
return instance_ptr;
}
};
singleton* singleton::instance_ptr = nullptr;
int main() {
singleton* a = singleton::get_instance();
singleton* b = singleton::get_instance();
cout << "俩指针指向的内存地址要相同啊" << endl;
cout << a << endl;
cout << b << endl;
delete a;
system("pause");
return 0;
}
//运行结果
我构造了啊
俩指针指向的内存地址要相同啊
000001D51C2D8E80
000001D51C2D8E80
我析构了啊
请按任意键继续. . .
懒汉式的问题:
没有保证线程安全;(给我加锁,加他)
需要手动delete,易发生内存泄漏;
易出现多次delete同一块内存空间,导致运行错误哦。(给我用智能指针,用他)
#include
#include
#include
using namespace std;
class singleton {
public:
typedef std::shared_ptr<singleton> Ptr; //简化类型名,Ptr是 std::shared_ptr同义词
~singleton() {
};
static Ptr get_instance() { //静态成员函数既可以在类内定义也可以在类外
mtx.lock(); //加锁
if (instance_ptr == nullptr) {
instance_ptr = Ptr(new singleton());
}
mtx.unlock();//释放锁
return instance_ptr;
}
private:
singleton() {
};
singleton(singleton&) {
};
singleton& operator=(const singleton&) {
};
static Ptr instance_ptr; //类变量
static mutex mtx;
};
//必须在类的外部定义和初始化每个静态成员
singleton::Ptr singleton::instance_ptr = nullptr;
mutex singleton::mtx;
问题:用户也必须使用共享指针;加锁增加开销,有时会失效?
#include
using namespace std;
class singleton {
public:
~singleton() {
};
static singleton& get_instance() {
static singleton instance; //变量位于内存的全局区
return instance;
}
private:
singleton() {
};
singleton(singleton&) {
};
singleton& operator=(const singleton&) {
};
};
int main() {
singleton& a = singleton::get_instance();
singleton& b = singleton::get_instance();
system("pause");
return 0;
}
如果是指变量的声明和定义
从编译原理上来说,声明是仅仅告诉编译器,有个某类型的变量会被使用,但是编译器并不会为它分配任何内存。而定义会为变量分配内存,可能会为变量赋初值。
变量只能会定义一次,但可以多次声明。
extern int i; //声明i,没有定义
extern int i = 0; //任何包含显示初始化的声明都是定义。
//!在函数体内部初始化由extern修饰的变量会报错
int j; //声明并定义j
参考1 参考2
修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。(变量的定义只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,但不能重复定义。)
extern 修饰变量的声明
如果文件 a.c 需要引用 b.c 中变量 int v,就可以在 a.c 中声明 extern int v,然后就可以引用变量 v。
但是需要注意变量的作用域。能够被其他模块以extern修饰符引用到的变量通常是全局变量。另外,extern int v可以放在a.c中的任何地方,不一定非要放在a.c的文件作用域的范围中,可以放在函数体内部,只不过这样只能在函数作用域中引用v罢了。
extern 修饰函数的声明
如果文件 a.cpp 需要引用 b.cpp 中的函数,比如在 b.cpp 中原型是 int fun(int mu),那么就可以在 a.cpp 中声明 extern int fun(int mu),然后就能使用 fun 来做任何事情。就像变量的声明一样,extern int fun(int mu)可以放在 a.cpp 中任何地方,而不一定非要放在 a.cpp 的文件作用域的范围中。对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件。
上才艺
// head.h
#pragma once
#include
extern int i;
extern char j;
extern void fun();
// head.cpp
#include "head.h"
int i = 7;
char j = 'a';
void fun() {
printf("call fun\n");
}
//main.cpp
#include
using namespace std;
extern int i;
extern char j;
extern void fun();
int main() {
cout << i << endl;
cout << j << endl;
fun();
system("pause");
return 0;
}
//输出
7
a
call fun
请按任意键继续. . .
extern 修饰符可用于指示 C 或者 C++函数的调用规范。
比如在 C++中调用 C 库函数,就需要在 C++程序中用 extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用 C 函数规范来链接。主要原因是 C++和 C 程序编译完成后在目标代码中命名规则不同。
void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机来实现函数重载的。
如果在C++中声明C中定义的函数时,未加extern "C"声明,那么在链接阶段,链接器会从.C文件生成的目标文件中寻找_foo_int_int这样的符号!如果加上了extern "C"声明,那么链接器在为C++代码寻找f(2,3)的调用时,寻找的是未经修改的符号名_foo。
所以,可以用一句话概括extern “C”这个声明的真实目的:实现C++与C及其它语言的混合编程。
tip1:
//main.cpp
#include
#include"head.h" //头文件的形式引用其他模块定义的变量
using namespace std;
int main() {
cout << i << endl;
cout << j << endl;
fun();
system("pause");
return 0;
}
tip2:使用extern和包含头文件来引用函数有什么区别呢?
转载自:原文链接,讲的很详细,有例子
静态局部变量
静态全局变量
定义在函数体外,用于修饰全局变量,表示该变量只在本文件可见(文件隔离)。
静态函数
准确的说,静态函数跟静态全局变量的作用类似。
除了C语言的几种用法外,static在c++中,
初始化只有一次,但是可以多次赋值,在程序开始之前,编译器会为静态变量在全局区分配内存。
const修饰变量
阻止一个变量被改变,可以使用 const 关键字,const对象一旦创建后其值就不能改变,所以const对象必须初始化。默认情况下,const对象被设定为仅在文件内有效,如果想在多个文件中共享const对象,必须在变量的定义前添加extern关键字。
类型转换符 const_cast 可以将const类型变量转换为非 const 类型;
! 如果类的数据成员是const或者引用,因为必须初始化,所以需要在构造函数的初始化列表中初始化。!
const修饰引用
常量引用:不能修改它所绑定的对象的值
const int c = 2;//顶层const
int& b = c; //错误,试图让一个非常量引用指向常量对象
const int& a = p; // 声明引用的const都是底层const
a = 3; //错误,试图修改常引用绑定的对象
(1)const int* (2)const p = &a;
// (1)常量指针,底层const表示指针所指对象是一个常量
// (2)指针常量,顶层const表示指针本身是常量
(1) int const *p1 或者 const int *p1 两种情况中 const 修饰*p1,所以理解为*p1 的值不可以改变,即不可以给*p1 赋值改变 p1 指向变量的值,但可以通过给 p 赋值不同的地址改变这个指针指向。底层指针表示指针所指向的变量是一个常量。
(2) int *const p2 中 const 修饰 p2 的值,所以理解为 p2 的值不可以改变,即 p2 只能指向固定的一个变量地址,但可以通过*p2 读写这个变量的值。顶层指针表示指针本身是一个常量
1const Stock& Stock::topval (2const Stock & s) 3const
(1)const修饰类成员函数的返回值
有时候必须指定其返回值为 const 类型,以使得其返回值不为“左值”。
(2)const修饰函数的形参
确保在函数内部不修改传递的参数的值,只有引用传递和指针传递可以用是否加 const 来重载。一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开来。
(3)const修饰类成员函数
表明此函数为常成员函数,不能修改类的成员变量。类的常对象只能访问类的常成员函数。(非常成员函数不能保证不修改类的数据成员)
Tip:
(1)const 成员函数可以访问非const对象的非const数据成员、const数据成员,
也可以访问 const 对象内的所有数据成员;(啥都能访问就是呗)
(2)非 const 成员函数可以访问非const对象的非const数据成员、const数据成员,
但不可以访问const对象的任意数据成员;
#define MAXTIME 1000 //简单的文本替换
#define max(x,y) (x)>(y)?(x):(y);
#define Add(a,b) a+b; //2*add(3+1)*2 被替换成 2*3+1*2 错误
#define pin (int*); //pin a,b 替换成 int *a,b; a是int型指针,b是int型变量
#include
using namespace std;
typedef double d; //d是double的同义词
typedef d a, *b; //a是double的同义词,b是double*的同义词
typedef char* p1; //p1是char*的同义词
#define p0 char*
int main(){
p1 e, f;
p0 g, h;
cout << typeid(e).name() << endl; //char * __ptr64
cout << typeid(f).name() << endl; //char * __ptr64
cout << typeid(g).name() << endl; //char * __ptr64
cout << typeid(h).name() << endl; //char
system("pause");
return 0;
}
#include
using namespace std;
void fun() {
typedef char* p1; //在此函数作用域有效
#define p0 char*
}
int main(){
p1 e; //编译失败,未定义标识符p1
p0 g;
cout << typeid(e).name() << endl;
cout << typeid(g).name() << endl;
system("pause");
return 0;
}
两者的区别是要看你在哪个角度理解
既然引用底层通过指针实现,引用能做的指针也能做,那为什么引入引用呢。参考
//指针传递
void f(int* p) {
printf("\n%x", &p); //形参指针的地址
printf("\n%x", p); //形参指针值,即传入的a的地址
printf("\n%x",*p);
int g = 6;
p = &g; //改变形参指针值,即改变指针的指向。对实参指针值不影响
printf("\n%x", &p);
printf("\n%x", p);
printf("\n%x", *p);
}
void main()
{
int a = 4;
int* b = &a;
printf("%x",&b); //指针的地址
printf("\n%x\n", b); //实参指针的值,变量a的地址
f(b);
printf("\n%x\n", a);
system("pause");
}
//引用传递
void f(int& p)
{
printf("\n%x", &p); //形参引用绑定到实参对象
printf("\n%x", p);
p = 6;
printf("\n%x", &p);
printf("\n%x", p);
}
void main()
{
int a = 4;
printf("%x",&a); //变量的地址
printf("\n%x", a); //变量的值
f(a);
printf("\n%x\n", a);
system("pause");
}
1.定义区别:
2.类的关系区别
3.规则上的不同
重定义(也成隐藏)例子讲解
(1)不在同一个作用域(分别位于派生类与基类) ;
(2)函数名字相同;
(3)返回值可以不同;
(4)参数不同时。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载以及覆盖混淆)。
(5)参数相同时,但是基类函数没有 virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆) 。
浅拷贝——仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么浅拷贝出来的对象也会相应改变。
深拷贝 —-在计算机中开辟了一块新的内存地址用于存放复制的对象
在某些状况下,类内成员变量需要动态开辟堆内存,如果实行浅拷贝,也就是把对象里的值完全复制给另一个对象,如 A=B。这时,如果 B 中有一个成员变量指针已经申请了内存,那 A 中的那个成员变量也指向同一块内存。这就出现了问题:当 B把内存释放了(如:析构),这时 A 内的指针就是野指针了,出现运行错误。(c++ primer P453)
Mat拷贝举例,深拷贝 b = a.clone();a.copyTo(b); 浅拷贝b = a;和 b(a);
源代码——>预处理——>编译——>优化——>汇编——>链接——>可执行文件 参考
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。
例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。因此需要链接器将有关的目标文件彼此相连接,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体
定义
函数指针指向的是函数而非对象,函数指针指向的是特殊的数据类型,函数的类型是由其返回的数据类型和其参数列表共同决定的,而函数的名称则不是其类型的一部分。
声明和赋值
要声明一个指向函数的指针,只需要用指针替换函数名即可。
一个具体函数的名字,如果后面不跟调用符号(即括号),则该名字就是该函数的指针(注意:大部分情况下,可以这么认为,但这种说法并不很严格)。
通过改变函数指针的指向,可以实现不同的函数调用
在c/c++中,没有规定long和int的长度具体是多少,只是规定long的长度不小于int的长度。其具体长度跟系统和编译器有关。
//在我的电脑上 64位操作系统,vs2015
int: 4 byte
long: 4 byte
long long: 8 byte
char: 1 byte
float: 4 byte
double: 8 byte
new/delect、野指针、内存泄漏、智能指针
除了全局区和栈区,每个程序还拥有一个内存池,被称为自由空间或堆区,程序用堆区存储动态分配的对象。动态对象的生存期由程序控制,只有当显示地被释放时,才会销毁。在c++中,动态内存的管理式通过一对运算符来完成的:
int *p = new int(3); delete p; int *p1 = new int[5]; delete[] p1;
参考
int *p1 = (int*)malloc(sizeof(int));
=> 根据传入字节数开辟内存,没有初始化
int *p2 = new int(0);
=> 根据指定类型int开辟一个整形内存,初始化为0
int *p3 = (int*)malloc(sizeof(int)*100);
=> 开辟400个字节的内存,相当于包含100个整形元素的数组,没有初始化
int *p4 = new int[100]();
=> 开辟400个字节的内存,100个元素的整形数组,元素都初始化为0
delete 释放new分配的 单个对象指针 指向的内存
delete[] 释放new分配的 对象数组指针 指向的内存,数组中的对象按逆序逐一调用destructor析构函数进行销毁!!
对于像int/char/long/int*/struct等等简单数据类型,由于对象没有destructor,所以用delete 和delete [] 是一样的!
但是如果是C++对象数组就不同了!使用delete的时候只调用了pbabe[0]的析构函数,而使用了delete[]则调用了数组中所有对象的析构函数。
你一定会问,反正不管怎样都是把存储空间释放了,有什么区别。
答:关键在于调用析构函数上。此程序的类没有使用操作系统的系统资源(比如:Socket、File、Thread等),所以不会造成明显恶果。如果你的类使用了操作系统资源,单纯把类的对象从内存中删除是不妥当的,因为没有调用对象的析构函数会导致系统资源不被释放,(1)如果是Socket则会造成Socket资源不被释放,最明显的就是端口号不被释放,系统最大的端口号是65535(216 _ 1,因为还有0),如果端口号被占用了,你就不能上网了,呵呵。(2)如果File资源不被释放,你就永远不能修改这个文件,甚至不能读这个文件(除非注销或重器系统)。(3)如果线程不被释放,这它总在后台运行,浪费内存和CPU资源。这些资源的释放必须依靠这些类的析构函数。所以,在用这些类生成对象数组的时候,用delete[]来释放它们才是王道。而用delete来释放也许不会出问题,也许后果很严重,具体要看类的代码了.
参考、参考
如何让类只支持静态对象创建和只支持动态对象创建?
前者重载new和delete为private属性,后者把构造和析构函数设为protected,由子类动态创建。
本菜鸡不确保以下代码的正确性。告辞
#include
#include
#include
#include
#include
using namespace std;
void* operator new(size_t sz) {
cout << "my reload operator new: " <<sz<< endl;
void* m = malloc(sz);
if (!m) {
cout << "my reload operator new error" << endl;
}
return m;
}
void operator delete(void* m) {
cout << "my reload operator free" << endl;
free(m);
}
class A {
public:
A() {
cout << "构造函数" << endl;
}
~A() {
cout << "析构函数" << endl;
}
};
int main()
{
cout << "----------- test 1 -----------" << endl;
int* m = new int(6);
delete(m);
cout << "----------- test 2 -----------" << endl;
char* c = new char[3](); //注意! new char[3] 不会默认初始化,这样strlen(c)长度不固定
cout << sizeof(c) << endl; //没有返回3,而是8,因为sizeof测量的是指针的大小不是数组的大小
cout << strlen(c) << endl;
char tem[] = "iloveyou";
cout << sizeof(tem) << endl;
cout << strlen(tem) << endl;
memcpy(c, tem, 2); // c最多存储两个字符
for (int i = 0;i < strlen(c);i++)
cout << c[i] << " ";
cout << endl;
delete[]c;
cout << "----------- test 3 -----------" << endl;
A* a = new A();
delete a;
cout << "----------- test 4 -----------" << endl;
A* aa = new A[2]();
delete []aa;
_CrtDumpMemoryLeaks(); //定位内存泄漏,经测试,这三种重载均没有内存泄漏
system("pause");
return 0;
}
/*输出
----------- test 1 -----------
my reload operator new: 4
my reload operator free
----------- test 2 -----------
my reload operator new: 3
8
0
9
8
i l
my reload operator free
----------- test 3 -----------
my reload operator new: 1
构造函数
析构函数
my reload operator free
----------- test 4 -----------
my reload operator new: 10 //????10
构造函数
构造函数
析构函数
析构函数
my reload operator free
请按任意键继续. . .
*/
class A {
public:
A() {
cout << "构造函数" << endl;
}
~A() {
cout << "析构函数" << endl;
}
//--------------------增加的部分down-----------------------------
void* operator new(size_t sz) {
cout << " class A's operator new: " << sz << endl;
void* m = malloc(sz);
return m;
}
void operator delete(void* m) {
cout << " class A's operator delete "<< endl;
free(m);
}
//--------------------增加的部分up-----------------------------
};
/*输出
----------- test 1 -----------
my reload operator new: 4
my reload operator free
----------- test 2 -----------
my reload operator new: 3
8
0
9
8
i l
my reload operator free
----------- test 3 -----------
class A's operator new: 1 //单个类对象的new调用的重载的类的operator new
构造函数
析构函数
class A's operator delete
----------- test 4 -----------
my reload operator new: 10 //类对象数组依然使用全局的operator new
构造函数
构造函数
析构函数
析构函数
my reload operator free
请按任意键继续. . .
*/
在3.3.2中虽然为类重载了operato new和operator delete,在创建类的单个对象时,new调用重载的operator new,但是创建类的对象数组时,依然调用全局的operator new用来为这个数组分配足够的内存。对此,我们可以通过为这个类重载运算符的数组版本,即operator new[]和operator delete[],来控制对象数组的内存分配。
class A {
public:
A() {
cout << "构造函数" << endl;
}
~A() {
cout << "析构函数" << endl;
}
void* operator new(size_t sz) {
cout << " class A's operator new: " << sz << endl;
void* m = malloc(sz);
return m;
}
void operator delete(void* m) {
cout << " class A's operator delete "<< endl;
free(m);
}
//----------------新增--------------
void* operator new[](size_t sz) {
cout << " class A's operator new[]: " << sz << endl;
void* m = malloc(sz);
return m;
}
void operator delete[](void* m) {
cout << " class A's operator delete[] " << endl;
free(m);
}
//---------------新增----------------
};
/*输出
----------- test 1 -----------
my reload operator new: 4
my reload operator free
----------- test 2 -----------
my reload operator new: 3
8
0
9
8
i l
my reload operator free
----------- test 3 -----------
class A's operator new: 1
构造函数
析构函数
class A's operator delete
----------- test 4 -----------
class A's operator new[]: 10 //new对象数组时调用的类的operator new[]
构造函数
构造函数
析构函数
析构函数
class A's operator delete[]
请按任意键继续. . .
*/
野指针:指向内存被释放的内存或者没有访问权限的内存的指针。
悬空指针:是指针最初指向的内存已经被释放了,以至于该指针仍旧指向已经回收的内存地址。
内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制。
后果
如何排除
解决方法
检查、定位内存泄漏
#define _CRTDBG_MAP_ALLOC
#include
#include
void main()
{
int* p = new int(5);
_CrtDumpMemoryLeaks();
system("pause");
}
/*
Detected memory leaks!
Dumping objects ->
{156} normal block at 0x0000029129C210F0, 4 bytes long.
Data: < > 05 00 00 00
*/
使用new和delete管理动态内存存在的三个常见问题:
(1)忘记delete释放内存,内存泄漏。
(2)使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误。
(3)同一块内存释放两次。自由空间可能被破坏。
为了更容易也更安全的使用动态内存,c++11标准库提供了智能指针类型来管理动态对象。智能指针的行为类似于常规指针,重要的区别是它负责自动释放所指向的对象。
unique_ptr、shared_ptr 和 weak_ptr。
unique_ptr 独占所指向对象,也就是说某个时刻只能有一个 unique_ptr 指向一个给定对象((通过禁止拷贝语义、只有移动语义来实现))。当它被销毁时它所指向的对象也被销毁.
shared_ptr允许多个指针指向同一对象,shared_ptr 会维护一个引用计数(use_count),只有最后一个 shared_ptr 也被销毁时,这个对象才真的被销毁。
week_ptr是一种不控制所指向对象生存期的智能指针,它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr 只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少.
why 内存对齐??
内存对齐原则
添加了#pragma pack(n)后规则就变成了下面这样:
struct A {
char a1; short a2; char a3; char a4;
long a5; char a6; int* a7; char a8;
};
struct B{
char a1; char a2; char a3;
};
int main() {
A a;
cout << sizeof(A) << endl;
printf("%x\n", &a.a1);
printf("%x\n", &a.a2);
printf("%x\n", &a.a3);
printf("%x\n", &a.a4);
printf("%x\n", &a.a5);
printf("%x\n", &a.a6);
printf("%x\n", &a.a7);
printf("%x\n", &a.a8);
cout<<&a.a8 - &a.a1<<endl;
system("pause");
return 0;
}
/*输出
24
10ff72c
10ff72e
10ff730
10ff731
10ff734
10ff738
10ff73c
10ff740
20
请按任意键继续. . .
*/
char long short int* int
在32位操作系统上 1 4 2 4 4 sizeof(A)=24 sizeof(B)=32
在64位操作系统上 1 4 2 8 4 sizeof(A)=32 sizeof(B)=3
#include
using namespace std;
class mystring {
public:
//1 构造函数1
mystring(int _cap = 10):capacity(_cap), size(0){
cout << "构造函数1" << endl;
s = new char[capacity];
}
//1 构造函数2
mystring(const char a[]){
cout << "构造函数2" << endl;
capacity = 2 * strlen(a); //原字符数组长度的两倍
size = strlen(a);
s = new char[capacity];
for (int i = 0;i < size;i++)
s[i] = a[i];
}
//2 拷贝构造函数
mystring(const mystring& src) {
cout << "拷贝构造函数" << endl;
capacity = src.capacity;
size = src.size;
s = new char[capacity];
for (int i = 0;i < size;i++)
s[i] = src.s[i];
}
//3 拷贝赋值运算符
mystring& operator = (const mystring& src) {
cout << "拷贝赋值运算符" << endl;
delete[] s; //删除原指针指向的数组
capacity = src.capacity;
size = src.size;
s = new char[capacity];
for (int i = 0;i < size;i++)
s[i] = src.s[i];
return *this; //注意返回值
}
//4 析构函数
~mystring(){
cout << "析构函数" << endl;
delete[] s;
s = nullptr;
}
//一些接口函数
void print() {
for (int i = 0;i < size;i++)
cout << s[i];
cout << endl;
}
private:
char* s;
int capacity;
int size;
};
int main() {
char c[] = "zlf";
mystring s0; //调用构造函数1
mystring s1(c); //调用构造函数2
mystring s2(s1); //调用拷贝构造函数
mystring s3 = s2; //调用拷贝构造函数 (注意)
mystring s4; //调用构造函数1
s4 = s3; //调用拷贝赋值运算符
system("pause");
return 0;
}
//输出
构造函数1
构造函数2
拷贝构造函数
拷贝构造函数
构造函数1
拷贝赋值运算符
请按任意键继续. . .
参考1、参考2
存在虚函数的类都有一个一维的虚函数表叫做虚表。每一个类的对象都有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
如果基类有虚函数,每一个派生类都有虚表。
虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
派生类的虚表中 虚函数地址的 排列顺序 和基类的虚表中虚函数地址排列顺序相同。
虚函数的代价
class A {
public: //类的用户可访问,派生类可访问
void pub_men() {};
private: //类的用户不可访问,派生类不可访问
void priv_men() {};
protected: //类的用户不可访问,派生类可访问
char prot_men() {};
};
class Pub_Derv :public A { //公有继承
/*可以理解为基类中的成员在派生类中如下
public:
void pub_men() {}; //所以 a可访问 ,Pub_Derv的派生类可访问
private:
void priv_men() {}; //所以 a不可访问 ,Pub_Derv的派生类不可访问
protected:
char prot_men() {}; //所以 a不可访问 ,Pub_Derv的派生类不可访问
*/
};
class Priv_Derv :private A { //私有继承,默认继承级别
/*可以理解为基类中的成员在派生类中如下
private:
void pub_men() {}; //所以 b不可访问 ,Priv_Derv的派生类不可访问
private:
void priv_men() {}; //所以 b不可访问 ,Priv_Derv的派生类不可访问
private:
char prot_men() {}; //所以 b不可访问 ,Priv_Derv的派生类不可访问
*/
};
class Prot_Derv :protected A { //受保护的继承
/*可以理解为基类中的成员在派生类中如下
protected :
void pub_men() {}; //所以 c不可访问
private:
void priv_men() {}; //所以 c不可访问
protected :
char prot_men() {}; //所以 c不可访问
*/
};
int main()
{
Pub_Derv a;
a.pub_men(); //可以访问
//a.priv_men(); //无权访问
//a.prot_men(); //无权访问
Priv_Derv b;
//b.pub_men(); //无权访问
//b.priv_men(); //无权访问
//b.prot_men(); //无权访问
Prot_Derv c;
//c.pub_men(); //无权访问
//c.priv_men(); //无权访问
//c.prot_men(); //无权访问
system("pause");
return 1;
}
A* P = &a; //对
p = &b; //错
p = &c; //错
标准库就是c++标准里面规定了的,任何编译器都必须要提供的库。编译器会有配套的标准库实现,不需要你另外去下载。
c++标准库分为两个部分:
(1)标准函数库:这个库由通用的、独立的、不属于任何类的函数组成,函数库继承自c语言。
(2)面向对象类库:这个库是类及其相关函数的集合。
#include
#include
int strlen(const char *p);//求p指向字符串的长度,不包括‘\0’
char* strcat(char* p1,const char* p2);//将p2连接到p1的末尾,p1的长度要够容纳p2
#include
#include
int abs(int x);
double power(double x,double,y);
double sin(double x);
double ceil(double x);//不小于x的最小整数
double floor(double x);//不大于x的最大整数
(4) 时间、日期和本地化
(5)动态分配
(6)宽字符函数
(7)其他
数据结构就是按照一定的逻辑结构组成的一批数据,使用某种存储结构将这批数据存储于计算机中,并在这些数据上定义了一个运算集合。
数组是连续的内存存储结构,链表是不连续的内存存储结构。(这两个是最基本的存储结构)。数据结构的内核要么是用数组实现,要么是用链表实现,要么就是数组+链表实现。
数据的逻辑结构可以分为两大类:一类是线性结构,另一类是非线性结构。
(1)线性结构:
(2)非线性结构:
(1)数组:
(2)链表:
(1)数组应用场景:
(2)链表应用场景:
极客时间-王争-数据结构与算法之美
平衡二叉查找树的严格定义:满足二叉查找树的特点,并且任意节点的左右子树高度相差不能大于1。
平衡二叉查找树设计的初衷是,解决普通二叉树在频繁的插入、删除等动态更新的情况下,出现时间复杂度退化的问题。所以,平衡二叉查找树中平衡的意思,其实就是让整棵树左右看起来比较对称,不会出现左子树很高、右子树很矮的情况。这样就能让整棵树的高度相对低一些,相应的插入删除查找等操作的效率高一些。
AVL树严格符合平衡二叉树的定义,左右子树高度相差不超过1,是高度平衡的二叉查找树。但是很多平衡二叉树并没有严格符合平衡二叉树的定义,比如红黑树,他从根节点到各叶子节点的最长路径,有可能比最短路径大一倍。因此红黑树不是严格意义上的平衡二叉查找树,它的高度近似log2n,插入、删除、查找的时间复杂度都是O(logn)。
一棵合格的红黑树需要满足以下几点:
在插入删除的过程中,第三点第四点要求可能被破坏,平衡调整实际上就是要把被破坏的第三点第四点恢复过来。调整的过程包含两种基础的操作:左右旋转和改变颜色。
左右旋太复杂了,老子不看了,哼
散列表用的是数组支持按照下标随机访问数据的特性,所以散列表是数组的一种扩展,由数组演化而来。
极客时间
散列函数计算得到的散列值是一个非负整数(数组下标从0开始,所以散列值要非负)
如果 key1=key2,那hash(key1)==hash(key2)
如果 key1!=key2,那hash(key1)!=hash(key2)
对于第三点,实际上我们无法找到完美的无冲突的散列函数,即便像业界著名的MD5、SHA、CRC等哈希算法,也无法完全避免这种散列冲突。而且,因为数组的存储空间有限,也会加大散列冲突的概率。
1 开放寻址法: 线性探测、 二次探测 、双重散列
map, set底层都是红黑树,key值无重复,自动排序
unordered_map,unordered_set底层是散列表,key值不重复,不排序
对于自定义的数据类型作为key,使用时
哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,时间复杂度为O(1);
而代价仅仅是消耗比较多的内存。
哈希表的查询时间虽然是O(1),但是并不是unordered_map查询时间一定比map短,因为实际情况中还要考虑到数据量,而且unordered_map的hash函数的构造速度也没那么快,所以不能一概而论,应该具体情况具体分析。