变量提供了一个拥有名字和为程序提供数据的可存储空间。而变量的数据类型决定着变量所能占据的内存空间的大小、数据怎么存储、被存储数据值的范围以及所能参与的运算。一般情况下,变量和对象这两个名词是等价的,均指一块能存储数据并具有某种类型的内存空间。
有关声明与定义:
由于C++支持分离式编译,也就是可以将程序分为多个文件来进行编译,当一个文件中需要使用另一个文件中定义的变量时,就需要先告知程序使用使用的是哪个定义的变量。 声明就是通告程序变量的名字,使得程序可以使用别处定义的变量。定义是创建与名字相关的实体(开辟内存空间,甚至赋初值)。声明和定义均规定了变量的类型和名字。
分类如下:
字面值常量就是我们一看它就知道它的值的常量,如42,0.5,‘a’,“abc”,true,false等。
给字面值常量添加前缀或者后缀,可以改变其默认类型:
字符和字面值常量:U, u, L, u8
整型字面值:u , U;l , L;ll , LL
浮点型字面值:F, f; l, L
利用关键字const对变量的类型加以限定,使对象变为常量,类似于C语言中的define功能。const对象只能执行不改变内容的操作。
const int bufsize = 1024;
const对象一旦创建后对象的值就不能改变,因此 const对象必须初始化。在对const对象进行初始化时,不必考虑用来初始化const对象的值是否是常量。
// 1.
const int a = 1;
// 2.
int b = 2;
const int c = b;
对常量的引用进行初始化时,只要初始值可以转换为引用的类型即可,不必考虑被引用的类型是否是常量。也就是说,常量引用可以引用一个非常量,但是与普通的引用不同的是不能通过常量引用来改变对象的值。
// 1.
int a = 1;
const int& b = a;
// 2.
const int c = 1;
const int& d = c;//表示这个引用(d)只能与c同名了:只代表c的这一块内存了
常量引用的理解:“常量引用”其实是“对 const 的引用”的简称。顾名思义,它把它所指向的对象看作是常量(不一定是常量),因此不可以通过该引用来修改它所指向的对象的值。
严格来说,并不存在常量引用,因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于 C++ 语言并不允许随意改变引用所绑定的对象,所以从这层意思上理解所有的引用又都算是常量。与普通引用不同的是,“常量引用”(即对 const 的引用)不能被用作修改它所绑定的对象。
对于常量的地址,只能使用指向常量的指针。而指向常量的指针也可以指向非常量,与对常量的引用类似,不能通过该指针改变对象的值,但不限制用其他方式改变对象的值。
//1.
const int a = 1;
int* aPtr = &a;
指针本身就是一个对象,因此可以将指针定义为常量。如果常量指针指向的是一个非常量,那么可以通过该指针改变对象的值。
顶层const可以表示任何对象是常量,而底层const常与指针和引用等复合类型有关。当执行对象的拷贝操作时,拷入和拷出的对象都必须具有相同的底层const资格,或者两个对象的类型能够相互转换。
int i = 0;
const int *const p = i;//左边的const是底层const,右边的const是顶层const
常量表达式指值不会改变并且在编译过过程就能得到就计算结果的表达式。字面值常量属于常量表达式;用常量表达式初始化的const对象也是常量表达式。可以将变量声明为constexpr类型,而且必须用常量表达式来初始化。
限定符constexpr作用于指针时,是将指针定为常量而不是指针指向的对象,这与const限定符的作用是不同的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HERuoyQ0-1632487370835)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210913185449744.png)]
局部变量:定义在函数内部的变量(函数的形参也是局部变量),只能在定义它的函数内部使用
全局变量:定义在函数外面的变量,所有函数都可以使用
静态变量:1.全局变量 2.前面加了“static”关键字的局部变量
对变量的分类依据可以把握两点:作用域(链接性)、存储位置(决定其生命周期)。
下面对链接性进行解释:
链接性 | 作用域 |
---|---|
外部链接性 | 全局作用域 |
内部链接性 | 文件作用域 |
无连接性 | 局部作用域(函数或代码块内部) |
下面对存储位置进行解释:
实质性差别在于存储位置:栈 or 静态存储区
编译器对两者的处理不一样,对自动变量,采用栈;对静态变量,编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序的执行期间一直存在。
static修饰的函数叫做静态函数,有两种静态函数:静态成员函数+全局静态函数
根据静态的相关特点来推出静态成员函数的特点:
调用这个函数不会访问或者修改任何对象(非static)数据成员。
理解:类的静态成员(变量和方法)属于类本身,在类加载的时候就会分配内存,可以通过类名直接去访问;非静态成员(变量和方法)属于类的对象,所以只有在类的对象产生(创建类的实例)时才会分配内存,然后通过类的对象(实例)去访问。
这样的static函数与普通函数的区别是:用static修饰的函数,限定在本源码文件中,不能被本源码文件以外的代码文件调用。而普通的函数,默认是extern的,也就是说它可以被其它代码文件调用。
在函数的返回类型前加上关键字static,函数就被定义成为静态函数。普通 函数的定义和声明默认情况下是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。因此定义静态函数有以下好处:
<1> 其他文件中可以定义相同名字的函数,不会发生冲突。
<2> 静态函数不能被其他文件所用。
给变量分配存储空间,可进行初始化,有两种方式:
int a = 1;
extern int a = 1;
不给变量分配存储空间。
如果在多个文件中使用外部变量(全局,且具有外部链接性),只需在一个文件中包含该变量的定义(重复定义也会出现问题),但在使用该变量的其他所有文件中,都必须使用关键字extern声明它。
由预处理器进行编译预处理——
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZC9hIIa0-1632487370837)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914133740644.png)]
c++提供的编译预处理主要有三种:
C++ 宏定义将一个标识符定义为一个字符串,源程序中的该标识符均以指定的字符串来代替。因此预处理命令后通常不加分号。这并不是说所有的预处理命令后都不能有分号出现。由于宏定义只是用宏名对一个字符串进行简单的替换,因此如果在宏定义命令后加了分号,将会连同分号一起进行置换。
习惯:
#undef<标识符>
使用方式:#define <宏标识符> <字符串>
在后面的源码中,用这些字符串来代替宏标识符。
宏表示的值可以是一个常量表达式,其中允许包括前面已经定义的宏标识符。
#define ONE 1
#define TWO 2
#define THREE (ONE+TWO)
//有关括号:不是必须的
six=THREE*TWO ——> six=(ONE+TWO)*TWO//此时如果没了括号就会计算错误,因为宏定义的实质是字符串的替换!!!
宏还可以代表一个字符串常量。
#define VERSION "Version 1.0 Copyright(c) 2003"
宏也可以代表一个常数。
#define SECONDS_PER_YEAR (60*60*24*365)UL
//表达式将使一个16位机的整形数溢出,因此要用到长整型符号L,告诉编译器该数是一个无符号长整型数UL
带参数的宏和函数调用看起来有些相似。
举例:#define Cube(x) (x)*(x)*(x)
可以时任何数字表达式甚至函数调用来代替参数x。这里再次提醒大家注意括号的使用。宏展开后完全包含在一对括号中,而且参数也包含在括号中,这样就保证了宏和参数的完整性。看一个用法:
#define Cube(x) (x)*(x)*(x)
int num=8+2;
volume=Cube(num);
不安全的用法:
volume=Cube(num++);
//展开之后如下:
volume=(num++)*(num++)*(num++);//结果是10*11*12,而不是10*10*10;
出现在宏定义中的#运算符把跟在其后的参数转换成一个字符串。有时把这种用法的#称为字符串化运算符。
#define PASTE(n) "adhfkj"#n
main(){
printf("%s\n",PASTE(15));
}
宏定义中的#运算符告诉预处理程序,把源代码中任何传递给该宏的参数转换成一个字符串。所以输出应该是adhfkj15。
##运算符用于把参数连接到一起。预处理程序把出现在##两侧的参数合并成一个符号。
#define NUM(a,b,c) a##b##c
#define STR(a,b,c) a##b##c
main(){
printf("%d\n",NUM(1,2,3));
printf("%s\n",STR("aa","bb","cc"));
}
//最后程序的输出为:
//123
//aabbcc
#include
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EAjZexN8-1632487370838)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914160206744.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ndDOiu3q-1632487370840)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914160219745.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h6Qn5STx-1632487370841)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914160235439.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lFaSWLGg-1632487370842)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914160249413.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eSO8ekUI-1632487370843)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914160310430.png)]
指定当前文件在编译时只包含一次。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ggj1cDP7-1632487370844)(C:\Polaris_LEARN\WORK-LEARNING\面经\c++\c++面经突击.assets\image-20210914160558468.png)]
首先要明白的是对象创建在堆和栈的区别:
明白了以上两点,对对象创建位置的限制便好理解了。
创建在堆,一般情况是要由程序员负责new(底层调用构造函数)和delete(底层调用析构函数)一个类的对象。若程序员没有new,如何让其也创建在堆上呢。
最直观的思想:避免直接调用类的构造函数。
当程序员直接调用类的构造函数时,理论上此时编译器要准备把该对象创建在栈上并负责该对象的删除(析构函数),因此编译器创建在栈上时要检查该对象的析构函数(其实是该对象的非静态成员进行检查,因为需要静态成员是建立在静态存储区的)能否被访问,此时发现不能访问析构函数,则编译器不会把这个对象创建在栈上。
因为析构函数是私有的,所以编译器不能创建在栈,因此对象的删除不能由编译器完成。此时需要一个public的destroy()函数,负责对象的删除。示例如下:
class A
{
public:
A() {}
void destory()
{
delete this;
}
private:
~A()
{
}
};
protected
构造函数设置为 protected,并提供一个 public 的静态函数来完成构造,而不是在类的外部使用 new 构造;将析构函数设置为 protected。原因:类似于单例模式,也保证了在派生类中能够访问析构函数。通过调用 create() 函数在堆上创建对象。
class A
{
protected:
A() {}
~A() {}
public:
static A *create()//设置为静态保证可以在只有类的时候便调用,否则产生对象了才能调用
{
return new A();
}
void destory()
{
delete this;
}
};
将 operator new() 设置为私有。原因:当对象建立在堆上时,是采用 new 的方式进行建立,其底层会调用 operator new() 函数,因此只要对该函数加以限制,就能够防止对象建立在堆上。
class A
{
private:
void *operator new(size_t t) {} // 注意函数的第一个参数和返回值都是固定的
void operator delete(void *ptr) {} // 重载了 new 就需要重载 delete
public:
A() {}
~A() {}
};
现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。
大家都知道,C++空类的内存大小为1字节,为了保证其对象拥有彼此独立的内存地址。非空类的大小与类中非静态成员变量和虚函数表的多少有关。
而值得注意的是,类中非静态成员变量的大小与编译器内存对齐的设置有关。
成员变量在类中的内存存储并不一定是连续的。它是按照编译器的设置,按照内存块来存储的,这个内存块大小的取值,就是内存对齐。
尽管内存是以字节为单位,但是大部分处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存,我们将上述这些存取单位称为内存存取粒度。
我们以对于结构体的内存对齐为例,描述内存对齐的规则:
注:很明显#pragma pack(n)作为一个预编译指令用来设置多少个字节对齐的。值得注意的是,n的缺省数值是按照编译器自身设置,一般为8,合法的数值分别是1、2、4、8、16。即编译器只会按照1、2、4、8、16的方式分割内存。若n为其他值,是无效的。
对于简单的数据类型,如int等,初始化和赋值是没有什么区别的。但对于比较复杂的自定义的一些结构,比如String、类等,初始化和赋值是有较大区别的。我们以类为例把初始化和赋值两者进行对比:
类的构造函数(默认构造函数、拷贝构造函数等)其实质上是服务于类的对象的初始化的,即我们在创建一个类的对象的时候,确定该对象的数据成员的值。该初始化都是在类的构造函数中完成的。有几种方式完成对类的初始化:
class A{
private:
int x;
int y;
public:
A(){
x = 1;
y = 1;
};
~A();
}
A a1;//调用无参构造函数
A a2 = a1;//调用默认拷贝构造函数。这里的“=”代表初始化,不是赋值。
A a3(a1);//调用默认拷贝构造函数
为类中的“=”进行重写之后,可以直接对类进行赋值,在这里我们以String为例:
class String {
public:
String( const char *init ); // intentionally not explicit!
~String();
String( const String &that );
String &operator =( const String &that );
String &operator =( const char *str );
void swap( String &that );
friend const String // concatenate
operator +( const String &, const String & );
friend bool operator <( const String &, const String & );
//...
private:
String( const char *, const char * ); // computational
char *s_;
} ;
//拷贝构造函数
String::String( const char * init ) {
if( !init )
init = "";
s_ = new char[ strlen(init)+1 ];
strcpy( s_, init );
}
//析构函数
String:: ~ String() { delete [] s_; }
//赋值操作
String & String:: operator = ( const char * str ) {
if( !str )
str = "";
char *tmp = strcpy( new char[ strlen(str)+1 ], str ); // 多了中间变量
delete [] s_; // 多了删除s_;
s_ = tmp; // 多一个赋值操作!现在是指向字符的指针,如果是个大对象,效率的差别可想而知.
return *this;
}
默认构造函数:在程序员没有重载构造函数时,编译器自动有一个默认无参构造函数。若程序员定义了构造函数(无论有参还是无参),编译器都不会再有无参构造函数了。
除了需要知道在对象被销毁时会自动调用析构函数外,还应注意以下事项:
创建并删除一个子类对象时,函数的调用顺序如下:
子类不能继承父类的构造函数,需要定义自己的构造函数。因此构造函数也不能重写。
在不显式声明的情况下,子类默认调用父类的无参构造函数。因此若父类只定义了一个有参构造函数(只要父类定义了构造函数(无论有参还是无参),就不会再有默认无参构造函数了),则在调用时便会报错。因此,此时子类的构造函数要显式表明要调用父类的哪一个构造函数(同时需要确定父类构造函数的参数是什么),代码示例如下:
class Base {
public:
Base(int argue){
cout << "我是Base有参构造函数!"<
析构函数可以重写并且建议重写(virtual)。
什么叫对象的拷贝:用其它对象的数据来初始化新对象的内存(因此也是构造函数)。
严格来说,对象的创建包括两个阶段,首先要分配内存空间,然后再进行初始化:
**拷贝构造函数的参数:**本类的const引用。
如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。
只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。
拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,添加 const 限制后,这个含义更加明确了。
另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。
参数:本类的引用,可以是const,可以不是const。(复制构造函数的参数加不加 const 对某些程序來说都一样。但加上 const 是更好的做法,这样复制构造函数才能接受常量对象作为参数,即才能以常量对象作为参数去初始化别的对象。注意:非常量对象可以转换为常量对象,但是常量对象不能转换为非常量对象。)
默认复制构造函数:如果类的设计者不写复制构造函数,编译器就会自动生成复制构造函数。
注意,默认构造函数(即无参构造函数)不一定存在,但是复制构造函数总是会存在。
默认的是浅拷贝,只拷贝一个副本。
对于动态分配内存,需要是深拷贝的。如果有指针,一般都是深拷贝。
有两种方法:普通重写和虚方法重写。
C++里面的四个智能指针: auto_ptr(已被c++11弃用), unique_ptr(c++11支持),shared_ptr(c++11支持), weak_ptr(c++11支持) 其中后三个是C++11支持,并且第一个已经被C++11弃用。
智能指针主要用于管理在堆上分配的内存,其相关处理方式可防止内存泄漏。
智能指针将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针是一个类,当超出了类的实例对象的作用域时,会自动调用对象的析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
采用所有权模式。
代码示例:
auto_ptr p1 (new string ("I reigned lonely as a cloud."));
auto_ptr p2;
p2 = p1; //auto_ptr不会报错.
此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。
auto_ptr的缺点:存在潜在的内存崩溃问题!
unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免内存泄露特别有用。
采用所有权模式。
unique_ptr p3 (new string ("auto")); //#4
unique_ptr p4; //#5
p4 = p3;//此时会报错!!
编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。尝试复制p3时会编译期出错,而auto_ptr能通过编译期从而在运行期埋下出错的隐患。因此,unique_ptr比auto_ptr更安全。
另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值(该unique_ptr还没有指针指向),编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,代码展示如下:
unique_ptr pu1(new string ("hello world"));
unique_ptr pu2;
pu2 = pu1; // #1 不允许
unique_ptr pu3;
pu3 = unique_ptr(new string ("You")); // #2 允许 临时右值
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
注:如果确实想执行类似与#1的操作,要安全地重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。尽管转移所有权后还是有可能出现原有指针调用(调用就崩溃)的情况。但是这个语法能强调你是在转移所有权,让你清晰的知道自己在做什么,从而不乱调用原有指针。
unique_ptr ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
注:事实上在执行完move函数之后,其内部把ps1指向了null(防止ps1变成野指针)。
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
下面给出一个简单的代码示例:
int main()
{
string *s1 = new string("s1");
shared_ptr ps1(s1);//把普通指针转化为shared_ptr
shared_ptr ps2;
ps2 = ps1;//两个指针共享同一片内存
cout << ps1.use_count() << endl; //2
cout<< ps2.use_count() << endl; //2
cout << ps1.unique() << endl; //0
string *s3 = new string("s3");
shared_ptr ps3(s3);
cout << (ps1.get()) << endl; //033AEB48
cout << ps3.get() << endl; //033B2C50
swap(ps1, ps3); //交换所拥有的对象
cout << (ps1.get())<
两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题——如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
代码示例如下:
class B; //声明
class A
{
public:
shared_ptr pb_;
~A()
{
cout << "A delete\n";
}
};
class B
{
public:
shared_ptr pa_;
~B()
{
cout << "B delete\n";
}
};
void fun()
{
shared_ptr pb(new B());
shared_ptr pa(new A());
cout << pb.use_count() << endl; //1
cout << pa.use_count() << endl; //1
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl; //2
cout << pa.use_count() << endl; //2
}
int main()
{
fun();
return 0;
}
可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减1,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A、B的析构函数没有被调用)运行结果没有输出析构函数的内容,造成内存泄露。如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_,改为weak_ptr pb_ ,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减1,同时pa析构时使A的计数减1,那么A的计数为0,A得到释放。
注意:我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(),因为pb_是一个weak_ptr,应该先把它转化为shared_ptr
shared_ptr p = pa->pb_.lock();
p->print();
weak_ptr 没有重载*和->,但可以使用 lock 获得一个可用的 shared_ptr 对象. 注意, weak_ptr 在使用前需要检查合法性.
expired 用于检测所管理的对象是否已经释放, 如果已经释放, 返回 true; 否则返回 false.
lock 用于获取所管理的对象的强引用(shared_ptr). 如果 expired 为 true, 返回一个空的 shared_ptr; 否则返回一个 shared_ptr, 其内部对象指向与 weak_ptr 相同.
use_count 返回与 shared_ptr 共享的对象的引用计数.
reset 将 weak_ptr 置空.
weak_ptr 支持拷贝或赋值, 但不会影响对应的 shared_ptr 内部对象的计数.
lambda表达式的语法形式定义如下:
[ capture ] ( params ) opt -> ret { body; };
其中 capture 是捕获列表,params 是参数表,opt 是函数选项,ret 是返回值类型,body是函数体。
代码举例:
auto f = [](int a) -> int { return a + 1; };
std::cout << f(1) << std::endl; // 输出: 2
lambda 表达式还可以通过捕获列表捕获一定范围内的变量:
代码示例(基本用法):
class A
{
public:
int i_ = 0;
void func(int x, int y)
{
auto x1 = []{ return i_; }; // error,没有捕获外部变量(不能访问类中的成员)
auto x2 = [=]{ return i_ + x + y; }; // OK,捕获所有外部变量(包括this)
auto x3 = [&]{ return i_ + x + y; }; // OK,捕获所有外部变量
auto x4 = [this]{ return i_; }; // OK,捕获this指针
auto x5 = [this]{ return i_ + x + y; }; // error,没有捕获x、y
auto x6 = [this, x, y]{ return i_ + x + y; }; // OK,捕获this指针、x、y
auto x7 = [this]{ return i_++; }; // OK,捕获this指针,并修改成员的值
}
};
int a = 0, b = 1;
auto f1 = []{ return a; }; // error,没有捕获外部变量
auto f2 = [&]{ return a++; }; // OK,捕获所有外部变量,并对a执行自加运算
auto f3 = [=]{ return a; }; // OK,捕获所有外部变量,并返回a
auto f4 = [=]{ return a++; }; // error,a是以复制方式捕获的,无法修改
auto f5 = [a]{ return a + b; }; // error,没有捕获变量b
auto f6 = [a, &b]{ return a + (b++); }; // OK,捕获a和b的引用,并对b做自加运算
auto f7 = [=, &b]{ return a + (b++); }; // OK,捕获所有外部变量和b的引用,并对b做自加运算
如上:lambda 表达式的捕获列表精细地控制了 lambda 表达式能够访问的外部变量,以及如何访问这些变量。
int a = 0;
auto f = [=]{ return a; }; // 按值捕获外部变量
a += 1; // a被修改了
std::cout << f() << std::endl; // 输出0
在这个例子中,lambda 表达式按值捕获了所有外部变量。**在捕获的一瞬间,a 的值就已经被复制到f中了。**之后 a 被修改,但此时 f 中存储的 a 仍然还是捕获时的值,因此,最终输出结果是 0。
如果希望 lambda 表达式在调用时能够即时访问外部变量,我们应当使用引用方式捕获。
按值捕获得到的外部变量值是在 lambda 表达式定义时的值。此时所有外部变量均被复制了一份存储在 lambda 表达式变量中。此时虽然修改 lambda 表达式中的这些外部变量并不会真正影响到外部,我们却仍然无法修改它们。——>会报错的!
那么如果希望去修改按值捕获的外部变量应当怎么办呢?这时,需要显式指明 lambda 表达式为 mutable:
int a = 0;
auto f1 = [=]{ return a++; }; // error,修改按值捕获的外部变量
auto f2 = [=]() mutable { return a++; }; // OK,mutable
注意:被 mutable 修饰的 lambda 表达式就算没有参数也要写明参数列表。
我们可以认为它是一个带有 operator() 的类,即仿函数。
存储lambda表达式:std::function
操作lambda表达式:std::bind()
std::function f1 = [](int a){ return a; };
std::function f2 = std::bind([](int a){ return a; }, 123);
C++11 lambda表达式精讲 (biancheng.net)
再看这个网站吧
面向对象的三大特性:
在同一个作用域中,如类成员函数之间的重载、全局函数之间的重载。
函数名相同、形参列表不同(参数类型、数目或顺序)、与返回类型无关。
与是否有virtual无关,有或无virtual都可以是重载,因为只看参数列表是否不同。
与const有关,因为const实际上是形参为const,这导致形参列表可能不同。
在父类和子类中
函数名相同、形参列表都相同(参数类型、数目或顺序)、返回类型相同或协变。
函数的const属性必须一致:const的属性是作用在传入参数中的,因此也代表参数列表需要一致。
函数的返回类型必须相同或协变。
有关协变的介绍:
首先理解三个概念:
在C++的虚函数重写中,派生类是基类的协变类型,协变类型的模板依然保持相同的性质,比如vector
应用协变的程序代码如下:
//正确示例:应用协变类型到虚函数重载中
#include
using namespace std;
class Base
{
public:
virtual Base* op()
{
cout << "Base";
return nullptr;
}
};
class Derive : public Base
{
public:
Derive* op() override
{
cout << "Derive";
return nullptr;
}
};
int main()
{
Base *a = new Derive();
a->op();
}
非指针或引用不存在协变一事,下面的虚函数重写是错误的。
函数名称与参数列表一致。
虚函数不能是模板函数。(可以了解一下什么是模板函数)
#include
using namespace std;
template
void Swap(T & x, T & y)
{
T tmp = x;
x = y;
y = tmp;
}
int main()
{
int n = 1, m = 2;
Swap(n, m); //编译器自动生成 void Swap (int &, int &)函数
double f = 1.2, g = 2.3;
Swap(f, g); //编译器自动生成 void Swap (double &, double &)函数
return 0;
}
访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)
被重写的函数不能是static的,必须是virtual的
virtual
修饰函数,那么通过父类指针访问的仍然会是访问父类的函数。不同作用域,比如派生类成员函数屏蔽与其同名的基类成员函数、类成员函数屏蔽全局外部函数。
只要函数名相同即可。
请注意,如果在派生类中存在与基类虚函数同返回值、同名且同形参的函数,则构成函数重写。
= new Derive();
a->op();
}
```
非指针或引用不存在协变一事,下面的虚函数重写是错误的。
函数名称与参数列表一致。
虚函数不能是模板函数。(可以了解一下什么是模板函数)
#include
using namespace std;
template
void Swap(T & x, T & y)
{
T tmp = x;
x = y;
y = tmp;
}
int main()
{
int n = 1, m = 2;
Swap(n, m); //编译器自动生成 void Swap (int &, int &)函数
double f = 1.2, g = 2.3;
Swap(f, g); //编译器自动生成 void Swap (double &, double &)函数
return 0;
}
访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)
被重写的函数不能是static的,必须是virtual的
virtual
修饰函数,那么通过父类指针访问的仍然会是访问父类的函数。不同作用域,比如派生类成员函数屏蔽与其同名的基类成员函数、类成员函数屏蔽全局外部函数。
只要函数名相同即可。
请注意,如果在派生类中存在与基类虚函数同返回值、同名且同形参的函数,则构成函数重写。