1、全局静态变量:
2、 局部静态变量
静态存储区,自动初始化为0。
作用域:在函数或者语句块结束的时候,作用域结束,但没有销毁,只是不能访问。再次被调用时,值不变;
3、静态函数
4、类的静态成员
5、类的静态函数
设计思想:C++是面向对象的语言,而C是面向过程的结构化编程语言
语法上:
C++具有封装、继承和多态三种特性
C++相比C,增加多许多类型安全的功能,比如强制类型转换、
C++支持范式编程,比如模板类、函数模板等
使用:
单一语句:
extern "C" double sqrt(double);
复合语句
extern "C"
{
double sqrt(double);
int min(int, int);
}
包含头文件,相当于头文件中的声明都加了extern "C"
extern "C"
{
#include
}
extern关键字
extern是C/C++语言中表明函数和全局变量的作用范围的关键字,该关键字告诉编译器,其申明的函数和变量可以在本模块或其他模块中使用。
extern int a; 仅仅是一个变量的声明,其并不是在定义变量a,也并未为a分配空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出错。
通常在模块的头文件中 加入 给其他模块 引用的函数和全局变量以关键字extern生命。如果模块B要引用模块A中定义的全局变量和函数时只需包含模块A的头文件即可。
extern对应的关键字是static,static表明变量或者函数只能在本模块中使用,因此,被static修饰的变量或者函数不可能被extern C修饰。
C语言的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast。
指针有自己的一块空间,而引用只是一个别名;
使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
指针可以被初始化为NULL(未知的值),而引用必须被初始化且必须是一个已有对象 的引用;
作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
可以有const指针,但是没有const引用;
指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能被改变;
指针可以有多级指针(**p),而引用至于一级;
指针和引用使用++运算符的意义不一样;
如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
检查内存泄漏神器valgrind https://zhuanlan.zhihu.com/p/75416381
class OnlyHeapClass
{
public:
static OnlyHeapClass* GetInstance(){
// 创建一个OnlyHeapClass对象并返回其指针
return (new OnlyHeapClass);
}
void Destroy(){
delete this;
}
private:
OnlyHeapClass() {}
~OnlyHeapClass() {}
};
int main()
{
OnlyHeapClass *p = OnlyHeapClass::GetInstance();
... // 使用*p
p->Destroy(); // 不能直接 delete p;
return 0;
}
Q:为什么要自己调用析构函数呢?对象结束生存期时不就自动调用析构函数了吗?什么情况下需要自己调用析构函数呢?
A:比如在析构之前必须做一些事情,但是用你类的人并不知道,那么你就可以重新写一个函数,里面把要做的事情全部做完了再调用析构函数。这样人家只能调用你这个函数析构对象,从而保证了析构前一定会做你要求的动作。
Q:什么情况下才要生成堆对象呢?
A:堆对象是new出来的,栈对象不是。何时该用动态,何时该用静态生成的问题。这个要根据具体情况。
Q:怎么生成堆对象?
A:C++是一个静态绑定语言,在编译过程中,所有的非虚函数调用都必须分析完成,即使是虚函数,也需要检查可访问性。当对象建立在栈上面时,是由编译器分配内存空间的,调用构造函数来构造对象。当对象使用完后,编译器会调用析构函数来释放栈对象所占的空间。
编译器管理了对象的整个生命周期。如果编译器无法调用类的析构函数,即析构函数是私有的,编译器无法调用析构函数来释放内存。所以上述例子类中必须提供一个Destroy函数,来进行内存空间的释放。类对象使用完成后,必须调用Destroy函数。
delete操作会调用析构函数,所以不能编译通过。提供一个Destroy成员函数,完成delete操作。在成员函数中,析构函数是可以访问的,当然detele操作也是可以编译通过。
Q:无法解决继承问题。
A:如果OnlyHeapClass作为其他类的基类,则析构函数通常要设为virtual,然后在子类重写,以实现多态。因此析构函数不能视为private。将析构函数设为protected可以有效解决这个问题,类外无法访问protected成员,子类则可以访问。为了统一,可以将构造函数也设为protected,然后提供一个public的static函数来完成构造,这样不用直接使用new而是使用一个函数来构造,使用一个函数来析构。
Q:构造和析构函数定义为私有的,阻止了用户在类域外对析构函数的使用。
A:具体表现为:
指针常量: int * const p;
修饰p,指针p不能改,不能指向其他地址。指针指向的值*p可改。
常量指针: const int *p = &a;
修饰int *,指向的内存区域的值是不能通过指针改变。但是指向的地址可改。
本质是一个指向常量的指针。指针指向的变量的值不可通过该指针修改,但是指针指向的值可以改变。
指向常量的常量指针: const int * const b = &a;
C++98 auto:用于声明变量为自动变量,自动变量意为拥有自动的生命期,这是多余的,因为就算不使用auto声明,变量依旧拥有自动的生命期: auto int b = 20 ; 加不加一样。
C++11 auto:在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型,类似的关键字还有decltype。
int a = 10;
auto au_a = a;//自动类型推断,au_a为int类型
cout << typeid(au_a).name() << endl; // int
1、用于代替冗长复杂、变量使用范围专一的变量声明。
不用auto:
std::vector vs;
for (std::vector::iterator i = vs.begin(); i != vs.end(); i++)
用auto。
for (auto i = vs.begin(); i != vs.end(); i++)
for循环中的i将在编译时自动推导其类型,而不用我们显式去定义
为何不直接使用using namespace std:
2、在定义模板函数时,用于声明依赖 模板参数 的变量类型。
template
void Multiply(_Tx x, _Ty y)
{
auto v = x*y;
std::cout << v;
}
由于不知道x*y的真正类型,只能用auto声明v
3、模板函数依赖于模板参数的返回值
template
auto multiply(_Tx x, _Ty y)->decltype(x*y)
{
return x*y;
}
当模板函数的返回值依赖于模板的参数时,无法在编译代码前确定模板参数的类型,
故也无从知道返回值的类型,这时可以使用auto。
auto在这里的作用也称为返回值占位,它只是为函数返回值占了一个位置,
真正的返回值是后面的decltype(_Tx*_Ty)。
decltype操作符用于查询表达式的数据类型,也是C++11标准引入的新的运算符,
其目的也是解决泛型编程中有些类型由模板参数决定,而难以表示它的问题。
为何要将返回值后置呢?
函数声明若为:decltype(x*y)multiply(_Tx x, _Ty y)
此时x,y还没声明,编译无法通过。
auto 变量在定义时 必须初始化,这类似于const关键字。
定义在一个auto序列的变量必须始终推导成同一类型。
auto a4 = 10, a5 = 20, a6 = 30;//正确
auto b4 = 10, b5 = 20.0, b6 = 'a'; //错误,b5、b6必须也是int型。
int a = 10;
int &b = a;
auto c = b; //c不是引用,类型为int而非int&
auto &d = b; //d是引用,类型为int&
const int a1 = 10;
auto b1= a1; //b1不是const,类型为int而非const int
const auto c1 = a1; //c1是const,类型为const int
const int a2 = 10;
auto &b2 = a2; //因为auto带上&,故不去除const,b2类型为const int
int a3[3] = { 1, 2, 3 };
auto b3 = a3; // b3为int*
int a7[3] = { 1, 2, 3 };
auto & b7 = a7; // b7为 int[3]
函数或者模板参数不能被声明为auto。 如 void func(auto a) 错误。
auto仅仅是一个占位符,并不是一个真正的类型。不能对auto使用一些以类型为操作数的操作符,如sizeof或者typeid。
作用:auto必须初始化,当不想初始化时用decltype。选择并返回操作数的数据类型。
1、基本用法
变量
int a = 2;
decltype(a) b; // dclTempA为int.
函数
int getsize(); // 可以不定义。decltype只分析,不调用。
decltype(getSize()) dclTempB; // dclTempB为int。
2、与const结合
const double a = 5.0;
decltype(a) tmp_a = 4.1; // 保留const,类型为const double
const double *const ptr_d = &a;
decltype(ptr_d) d = &a; // 类型为为const double * const*
cout<
3、与引用结合
int a = 0, &ref_a = a;
decltype(ref_a) dcl_a = a; // dcl_a为引用
decltype(ref_a) dcl_b = 0; // dcl_b为引用,必须绑定变量,编译不过
decltype(ref_a) dcl_c; // dcl_c为引用,必须初始化,编译不过
decltype((a)) dcl_d = a; // dcl_a为引用。双层括号表示引用。
const int b = 1, &ref_b = b;
decltype(ref_b) dcl_e = a; // dcl_a为常量引用,绑定普通变量
decltype(ref_b) dcl_f = b; // dcl_b为常量引用,绑定常量
decltype(ref_b) dcl_g = 0; // dcl_c为常量引用,绑定临时变量
decltype(a) dcl_h = a; // dcl_a为常量引用,必须初始化,编译不过
decltype((a)) dcl_d = b; // dcl_a为常量引用。双层括号表示引用。
4、与指针结合
int a = 2;
int *ptr_a = &a;
decltype(ptr_a) dcl_a; // dcl_a为int *指针
decltype(*ptr_a) dcl_b; // dcl_b为引用。其中*表示解引用操作,引用必须初始化,编译不过
decltype和auto都可以用来推断类型,但是二者有几处明显的差异:
volatile 是一个类型修饰符。
1、易变性:在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的volatile变量的寄存器内容,而是重新从内存中读取。
变量有时可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。
正常情况下,在代码运行过程中,编译器会优化代码。
加入volatile 后。编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
volatile 可以保证对特殊地址的稳定访问。
2、不可优化性:不要对volatile 变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。
void main(){
int a;
a = 1;
printf("%d", a); // 编译器发现a变量是无用的,将a退化为常量。
// 对a加入volatile后,变量始终存在,需要从内存读到寄存器中,再调用printf函数。
}
3、顺序性:
多线程中简单案例:
volatile bool bStop = FALSE;
线程1:
bStop = TRUE;
while(bStop); // 另一个线程通过将bStop复制为False跳出该循环。
// 但是由于bStop已经放置在寄存器中了,如果没有volatile将永远无法跳出。
线程2:
while( !bStop ) { ... }
bStop = FALSE;
return;
看起来合理,实际是有错误的。
顺序性案例
案例1:
将 thread1 作为主线程,等待 thread2 准备好 value。
因此,thread2 在更新 value 之后将 flag 置为真,而 thread1 死循环地检测 flag。
简单来说,这段代码的意图希望实现 thread2 在 thread1 使用 value 之前执行完毕这样的语义。
// global shared data
bool flag = false;
thread1() {
flag = false;
Type* value = new Type(/* parameters */);
thread2(value);
while (true) {
if (flag == true) {
apply(value);
break;
}
}
thread2.join();
if (nullptr != value) { delete value; }
return;
}
thread2(Type* value) {
// do some evaluations
value->update(/* parameters */);
flag = true;
return;
}
问题:
1、在 thread1 中,flag = false 到 while 死循环里,没有任何机会对 flag 的值做修改。
因此编译器可能会将 if (flag == true) 的内容全部优化掉。
2、在 thread2 中,尽管逻辑上 update 发生在 flag = true 之前,但编译器和 CPU 并不知道。
因此 flag = true 可能发生在 update 完成之前,
因此 thread1 执行 apply(value) 时可能 value 还未准备好。
案例2:
将 flag 声明为 volatile。
1、在 thread1 中,flag == true 是对 volatile 变量的访问,故而 if 不会被优化消失。
2、由于value不是volatile,编译器仍有可能将 thread2 中的 update 和对 flag 的赋值交换顺序。
3、由于flag 声明为 volatile,这样使用 volatile 不仅无法达成目的,反而会导致性能下降
案例3:
对 value 也加以 volatile 关键字修饰。注意 value 定义和 thread2 的参数都需要加volatile。
1、都加了volatile,编译器不会交换他们的顺序。
2、代码最终是要运行在 CPU 上,CPU 是乱序执行(out-of-order execution)的。
在 CPU 执行时,value 和 flag 的赋值仍有可能是被换序了的(store-store)。
3、thread2中,value = new Type()做了三件事:
分配一块 sizeof(Type) 大小的内存;
在这块内存上,执行 Type 类型的初始化;
将这块内存的首地址赋值给 value。
4、对于编译器来说,这些工作都是改表达式语句的求值和副作用,因此不会与 flag 赋值语句换序。
5、在 CPU 乱序执行下,有可能发生 value 和 flag 已赋值完毕,内存里尚未完成 Type 初始化的情况。
6、x86 和 AMD64 架构的 CPU 只允许 sotre-load 乱序,而不会发生 store-store 乱序;
或者在诸如 IA64 架构的处理器上,对 volatile-qualified 变量的访问采用了专门的指令。
但是,使用 volatile 会禁止编译器优化相关变量,从而降低性能。
不建议依赖 volatile 在这种情况下做线程同步。
此外,这严重依赖具体的硬件规范,不可靠。
案例4:
需要做到的:
一:flag 相关的代码块不能被轻易优化消失。
二:要保证线程同步的 happens-before 语义。
本质上,设计使用 flag 本身也就是为了构建 happens-before 语义。
如有其他不用 flag 的办法解决问题,那么 flag 就不重要。
方法一:使用原子操作
std::atomic flag = false; // #include
由于对 std::atomic 的操作是原子的,同时构建了良好的内存屏障。
因此整个代码的行为在标准下是良定义的。
方法二:使用互斥量和条件变量
std::mutex m; // #include
std::condition_variable cv; // #include
bool flag = false;
thread1() {
flag = false;
Type* value = new Type(/* parameters */);
thread2(value);
std::unique_lock lk(m);
cv.wait(lk, [](){ return flag; });
apply(value);
lk.unlock();
thread2.join();
if (nullptr != value) { delete value; }
return;
}
thread2(Type* value) {
std::lock_guard lk(m);
// do some evaluations
value->update(/* parameters */);
flag = true;
cv.notify_one();
return;
}
上图为C/C++ Volatile关键词的使用的伪代码。
主要过程:
1、声明一个Volatile的flag变量。
2、Thread1 执行sth操作后,修改flag。Thread2 不断读取flag,基于sth操作,执行other th。
3、flag = true后,能不能保证Thread1中的something操作一定已经完成了?
案例1:非Volatile变量
C/C++编译器最基本优化原理:保证一段程序的输出,在优化前后无变化。
如果编译器改变了A,B变量的赋值顺序,但是foo(A, B)函数的执行结果不变,这么做是可行的,
案例2:一个Volatile变量
变量B被声明为volatile变量。
C/C++ Volatile变量,与非Volatile变量之间的操作,是可能被编译器交换顺序的。
结论: 通过flag = true,来假设something一定完成是不成立的。
案例3:
将A,B两个变量都声明为volatile变量。A,B赋值乱序的现象消失。
C/C++ Volatile变量间的操作,是不会被编译器交换顺序的。
案例:happens-before
由于Thread1中的代码执行顺序发生变化,flag = true被提前到something之前进行。
整个Thread2的假设全部失效。
能不能将something中所有的变量全部设置为volatile呢?
CPU本身为了提高代码运行的效率,也会对代码的执行顺序进行调整。
这就是所谓的CPU Memory Model (CPU内存模型)。
X86体系(X86,AMD64)也会存在指令乱序执行的行为:
Store-Load乱序,读操作可以提前到写操作之前进行。
下图构建一个happens-before语义。
保证Thread1代码块中的所有代码,一定在Thread2代码块的第一条代码之前完成。
可以使用:Mutex、Spinlock、RWLock等。但C/C++ Volatile关键词不能保证这个语义
Java的Volatile也有这三个特性,但最大的不同在于:第三个特性,”顺序性”,Java的Volatile有很极大的增强——Java Volatile变量的操作,附带了Acquire与Release语义。
1、对于Java Volatile变量的写操作,带有Release语义,所有Volatile变量写操作之前的针对其他任何变量的读写操作,都不会被编译器、CPU优化后,乱序到Volatile变量的写操作之后执行。
2、对于Java Volatile变量的读操作,带有Acquire语义,所有Volatile变量读操作之后的针对其他任何变量的读写操作,都不会被编译器、CPU优化后,乱序到Volatile变量的读操作之前进行。
简而言之:
1、Java Volatile对于编译器、CPU的乱序优化,限制的更加严格了。
2、Java Volatile变量与非Volatile变量的一些乱序操作,也同样被禁止。
3、Java Volatile,能够用来构建happens-before语义。
const 有常量指针和指针常量的说法,volatile 也有相应的概念
1、在内嵌汇编操纵栈时,编译器无法识别的变量改变。
2、多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。
按照 Hans Boehm & Nick Maclaren 的总结,volatile
只在三种场合下是合适的。
setjmp
和 longjmp
)相关的场合。比如 if (nullptr == value) 或者 if (nullptr != value)
主要是为了防止少写一个=号。 写出value = nullptr
void *malloc(size_t size)
案例:double *p = (double *)malloc(sizeof(double));
注意:返回的是指针,指向在堆区分配的内存空间
void free(void *ptr)
注意:
1、参数是指针,其指向的内存块必须是之前通过调用 malloc、calloc 或 realloc 进行分配内存的。
2、如果传递的参数是一个空指针,则不会执行任何动作。
3、如果指向一个非堆区内存会报warning
1、for range
for (const auto &c : ivec) cout << c << " ";
2、传统for
for (auto i = 0; i != ivec.size(); ++i) cout << ivec[i] << " ";
3、迭代器 // auto 是 vector::const_iterator
for (auto it = ivec.cbegin(); it != ivec.cend() ; ++it) cout << *it << " ";
4、for_each + lambda
for_each(ivec.cbegin(), ivec.cend(), [](const int& c) {cout << c << " "; });
5、ostream_iterator
ostream_iterator out_iter(cout, " ");
copy(ivec.cbegin(), ivec.cend(), out_iter);
3.1、template function: 针对别的container也行,比如list等.
template
void display_container(const T& container) {
for (auto it = container.cbegin(); it != container.cend(); ++it)
cout << *it << " ";
}
display_container(ivec);
4.1、 for_each + functor
template
struct Display {
void operator() (const elementType& element) const {
cout << element << " ";
}
};
for_each(ivec.cbegin(), ivec.cend(), Display());
for_each()事实上是個 function template
template
Function for_each(InputIterator beg, InputIterator end, Function f) {
while(beg != end)
f(*beg++);
}
Object Oriented 与for_each 搭配
简单来说,异或就是没有进位的加法。
减法的实现原理:计算机是不会做减法的,它是把后一个数变成负数,通过加负数来运算。
比如3-2就是3+(-2)。
加法的实现是通过数字的补码相加,这里又涉及原码,反码和补码。
原码就是那个二进制编码,比如3就是0000,0011,-3是1000,0011(第一位是符号位)。
反码就是正数不变,负数除了符号位,每一位都反过来,比如3的反码就是本身,-3就变成1111,1100。
补码还是正数不变,负数是反码+1,比如-3就是1111,1101。
接下来讲减法就比较简单了,将两个数的补码相加即可。
对于3+(-2),就是0000,0011+1111,1110,结果是1,0000,0001。
前面那个1自动溢出,剩下就是1了。
在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
void *memset(void *s, int ch, [size_t];
清零: memset(a,0,sizeof(a));
无穷大: memset(a,0x3f,sizeof(a));
最大值: memset(a,0x7f,sizeof(a)); 第一位为符号位,不能为1,剩下全是1
无穷大为0x3f的原因:
前提:
1、int最大为:2147483647,大概2e9。
2、memset是按照字节操作的,0x3f 在int中就是 0x3f3f3f3f。
原因:
1、0x3f3f3f3f,十进制是1061109567,1e9,一般的数不会比这个大,可以代表无穷。
2、0x3f3f3f3f+0x3f3f3f3f=2122219134。不会爆int,满足:无穷大+无穷大=无穷大
3、memset是按字节操作的,将某个数组全部赋值为无穷大。(解决图论问题时邻接矩阵的初始化)
'0' 和 '1' 转换: char c ^= 1;
大小写转换: char c ^= 32;
scanf("%c", &op); 在 %c 前面加空格可以过滤掉空格、tab、换行等。
最好把op协程字符串
char op[2];
scanf("%s", &op); 会自动过滤掉空格、tab、换行等。
char *do_strcpy(char *dst, const char *src)
{
assert(dst != NULL);
assert(src != NULL);
char *ret = dst;
while((* dst++ = * src++) != '\0') // 运算符优先级++高于*
;
return ret;
}
1、判断源字符串和目的字符串是否为空
2、现将目的地址指针保存起来,方便输出
3、源字符串const,不能改变
考虑字符串重叠情况
#include
using namespace std;
char *do_strcpy(char *dest, const char *src){
assert(dest != NULL);
assert(src != NULL);
char *p = dest;
int count = strlen(src);
if (dest <= src || dest >= (src + count))
while ( (*dest++ = *src++) != '\0');
else{
char tmp = *src;
dest += count;
src += count;
while ( (*dest-- = *src--) != tmp);
}
return p;
}
int main(){
char a[10];
char b[] = "1234";
cout << do_strcpy(a, b) << endl;
}
NULL:
NULL是一个宏定义,在c和c++中的定义不同,c中NULL为(void*)0,而c++中NULL为整数0。
所以在c++中int *p=NULL; 实际表示将指针P的值赋为0,而c++中当一个指针的值为0时,认为指针为空指针。
//C语言中NULL定义
#define NULL (void*)0 //c语言中NULL为void类型的指针,但允许将NULL定义为0
//c++中NULL的定义
#ifndef NULL
#ifdef _cpluscplus //用于判定是c++类型还是c类型,详情看上一篇blog
#define NULL 0 //c++中将NULL定义为整数0
#else
#define NULL ((void*)0) //c语言中NULL为void类型的指针
#endif
#endif
nullptr:
nullptr是一个字面值常量,类型为std::nullptr_t,空指针常数可以转换为任意类型的指针类型。
在c++中(void *)不能转化为任意类型的指针,即 int p=(void)是错误的,但int *p=nullptr是正确的,原因
对于函数重载:若c++中 (void *)支持任意类型转换,函数重载时将出现问题下列代码中fun(NULL)将不能判断调用哪个函数
void fun(int i){cout<<"1";};
void fun(char *p){cout<<"2";};
int main()
{
fun(NULL); //输出1,c++中NULL为整数0
fun(nullptr);//输出2,nullptr 为空指针常量。是指针类型
}
1、char[] 分配是在栈上,而char* 则分配在字符串常量区
2、堆和栈的理论知识
申请方式:
申请后系统响应
申请大小限制
申请效率比较
堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令的地址,然后是函数的各个参数。大多数 C 编译器中,参数是由右往左入栈的,然后是函数中的局部变量。静态变量不入栈。本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
存取效率的比较
总结:
int *p = 1; 错误。int *p 定义了一个指针p,然而并没有指向任何地址,所以当使用 *p 时是没有任何地址空间对应的,所以 *p=1 就会导致,不知道把这个1赋值给哪个地址空间了。
int *p; p = 1; 错误。 int *p; 定义了一个指针p, p = 1; 意思是将一个存地址为1的地址赋值给p,所以这个是可行的。但是这个操作是不安全的。
int atoi(const char *str)
字符串转换为一个整数,转不了返回0
int val;
char str[20];
strcpy(str, "98993489");
val = atoi(str);
动态内存管理经常会出现两种问题:
智能指针
#include
std::unique_ptr -- single ownership
std::shared_ptr -- shared ownership
std::weak_ptr -- temp / no ownership
1、new一个对象指针
2、把指针传给一个函数,函数需要考虑以下问题
谁 own 对象指针
对象指针 生命周期多长?
对象指针是否会被销毁,我要不要销毁
3、如果 函数没有删除的话,指针就泄露了。因为foo结束后,e指针就得不到了。
void func(Entity* e){
}
void foo(){
Entity* e = new = Entity();
func(e);
}
foo();
改进:
void func(Entity* e){
// func own e;
// e 会自动销毁
}
void foo(){
auto e = std::make_unique(); //创建智能指针
func(std::move(e)); // 把 ownership 交给 func
}
foo();
unique_ptr 使用方法
std::unique_ptr e = new Entity(); // no,不能直接赋值
std::unique_ptr e(new Entity()); // yes,调用构函
std::unique_ptr e = std::make_unique(); // 更好
std::unique_ptr e1 = e2; // no,不能赋值
std::unique_ptr e1 = std::move(e2); // yes,把ownership给e2,e1变空指针
func(std::move(e)); // 函数参数也不能直接传,要用move递交ownership
shared_ptr 在多线程中,当use_count==0时销毁
std::shared_ptr e(new Entity()); // yes,调用构函
std::shared_ptr e = std::make_shared(); // 更好
std::shared_ptr e1 = e2; // 可以拷贝,use_count + 1
std::shared_ptr e1 = std::move(e2); // 把ownership给e2,e1变空指针,use_count 不加
func(e); // 函数参数可以直接传,use_count + 1
func(std::move(e)); // use_count 不加
weak_ptr 很少用
auto e1 = std::make_shared();
std::weak_ptr ew = e1;
if (std::shared_ptr e2 = ew.lock()) # 如果用 use_count ++
e2 -> func(); // 用完 use_count --
结论:
总结:引用计数是原子的,但是对象的读写不是
https://blog.csdn.net/D_Guco/article/details/80155323
字节对齐主要是为了提高内存的访问效率,比如intel 32位cpu,每个总线周期都是从偶地址开始读取32位的内存数据,如果数据存放地址不是从偶数开始,则可能出现需要两个总线周期才能读取到想要的数据,因此需要在内存中存放数据时进行对齐。
#include
struct A{
char a;
int b;
short c;
};
在32位机器上char 占1个字节,int 占4个字节,short占2个字节,一共占用7个字节
实际上 12字节, 比计算的7多了5个字节。
内存对齐主要遵循下面三个原则:
指定大小(默认4)和 自身大小的最小值。默认对齐大小 4 = 最宽数据大小 int 4。
起始地址为0x0000
a 1字节,偏移量 1
b 4字节,偏移量 1 不能整除 4,增加3位。偏移量 8。
c 2字节,偏移量 8,总大小10字节,根据3,补偏移量 2,一共12个字节。
每个特定平台上的编译器都有自己的默认“对齐系数”。可以通过预编译命令#pragma pack(n)
在#pragma pack(1)时,以1个字节对齐时,属于最简单的情况,结构体大小是所有成员的类型大小的和。
源代码–>预处理–>编译–>优化–>汇编–>链接–>可执行文件
扩展: **静态链接和动态链接?**请自查
静态链接:一个目标文件集合;
动态链接:函数代码放到被称作动态链接库或共享对象的某个目标文件中,可执行文件执行时,动态链接库的全部内容映射到运行时相应进程的虚地址空间,动态链接程序根据记录找到对应代码。
引用就是某一变量的一个别名,对引用的操作与对变量直接操作完全一样。引用的声明方法:
指针利用地址,它的值直接指向存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。
区别:
1、指针有自己的一块空间,而引用只是一个别名;
2、使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
3、指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用;
4、作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象;
5、可以有const指针,但是没有const引用;
6、指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;
7、指针可以有多级指针(**p),而引用至于一级;
8、指针和引用使用++运算符的意义不一样;
9、如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。
1、封装
隐藏对象的属性和实现细节,仅对外提供公共访问方式,将变化隔离,便于使用,提高复用性和安全性。
2、继承
提高代码复用性;继承是多态的前提。
3、多态
父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。
1、单一职责原则SRP(Single Responsibility Principle)
类的功能要单一,不能包罗万象,跟杂货铺似的。
2、开放封闭原则OCP(Open-Close Principle)
一个模块对于拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改,哼,一万个不乐意。
3、里式替换原则LSP(the Liskov Substitution Principle LSP)
子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活。哈哈~~
4、依赖倒置原则DIP(the Dependency Inversion Principle DIP)
高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。就是你出国要说你是中国人,而不能说你是哪个村子的。比如说中国人是抽象的,下面有具体的xx省,xx市,xx县。你要依赖的是抽象的中国人,而不是你是xx村的。
5、接口分离原则ISP(the Interface Segregation Principle ISP)
设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好。就比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多。
面向过程:
面向对象:
语法上可以,但不能。
析构函数调用:
前一种情况抛出异常不会有无法预料的结果,可以正常捕获;
后一种情况下,因为函数发生了异常而导致函数的局部变量的析构函数被调用,析构函数又抛出异常,本来局部对象抛出的异常应该是由它所在的函数负责捕获的,现在函数既然已经发生了异常,必定不能捕获,因此,异常处理机制只能调用terminate()。
若真的不得不从析构函数抛出异常,应该首先检查当前是否有仍未处理的异常,若没有,才可以正常抛出。
当在某一个析构函数中会有一些可能(哪怕是一点点可能)发生异常时,那么就必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外
他们的作用是一样的,唯一的区别是,当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的,memcpy不保证拷贝的结果的正确。
第三个函数的功能也是复制内存,但是如果遇到某个特定值时立即停止复制。
虚函数定义:
为什么用虚函数,作用:
单一继承:class Book : public Library { … };
多重继承:class iostream : public istream , public ostream { … };
虚继承:class Book : virtual public Library { … };
虚继承下,无论基类派生了多少次,只会存在一个实例,称作subobject。
如,iostream中只有virtual ios base class一个实例。 iostream : istream, ostream : ios
拷贝构造函数
构造函数抛出异常
设置结构体的边界对齐为1个字节,也就是所有数据在内存中是连续存储的。
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。
比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈