一、C语言基础
include时双引号与尖括号
区别在于查找头文件时的路径顺序不同
尖括号
1.编译器设置的头文件路径(-I 显示指定头文件路径);
2.系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径;
双引号
0.当前头文件目录;
1.编译器设置的头文件路径(-I 显示指定头文件路径);
2.系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径;
变量的声明、定义、初始化
变量定义:用于为变量分配存储空间
,还可为变量指定初始值。程序中,变量有且仅有一个定义。
变量声明:用于向程序表明变量的类型和名字,不一定分配存储空间
。
初始化:为变量指定初始值。
extern int i; //声明,不是定义
int i; //声明,也是定义,未初始化
int i=100; // 声明,定义,以及初始化
union
union的所有成员占用同一段内存,修改一个成员会影响其余所有成员
!
举例=> 下面检测大小端代码,修改value会影响union_bytes,因为可以判断大小端
void byteorder()
{
union
{
short value;
char union_bytes[ sizeof( short ) ];
} test;
test.value = 0x0102;
if ( ( test.union_bytes[ 0 ] == 1 ) && ( test.union_bytes[ 1 ] == 2 ) )
{
printf( "big endian\n" );
}
else if ( ( test.union_bytes[ 0 ] == 2 ) && ( test.union_bytes[ 1 ] == 1 ) )
{
printf( "little endian\n" );
}
else
{
printf( "unknown...\n" );
}
}
free时如何知道要释放的内存大小
参考:free()函数如何知道要释放的空间大小?
需要视内存分配算法而定,而内存分配算法又有很多种……针对这个问题,比较常见的一种做法是:空间的大小记录在参数指针指向地址的前面,free的时候通过这个记录即可知道要释放的内存有多大
。
左值右值
- 左值
左值是用来指明一个对象的表达式。最简单的左值就是变量名称。一个左值表示一个对象,它可以出现在赋值运算符(assignment operator)的左边
,例如“左表达式=右表达式”; - 右值
其他表达式(那些表示一个值但不指明一个对象的),被类似地称为右值。右值是可以出现在赋值运算符右边而不是左边的表达式.例如,常量和算术表达式
。表达式结束后右值将不再存在
;
左结合右结合
比如说 + 是左结合的,因为某个运算分量的左右两侧都有 + 时,该运算分量属于其左边的 +
C语言中的static
- 修饰函数的作用
将函数隐藏在当前编译单元内; 若没有static修饰,则是全局可见的! - 修饰变量的作用
1. 将变量隐藏在当前编译单元内(对于局部变量就是局部作用域,对于全局变量就是当前文件);
2. 保持变量内容的持久:static变量和全局变量都是在静态存储区上
,不会随着函数调用的结束而清空;
3. 默认初始化为0(静态存储区上的数据都是默认初始化为0); - 小结
static声明局部变量
,使其变为静态存储方式(静态数据区),但作用域不变;
用static声明外部变量
,其本身就是静态变量,这只会改变其连接方式,使其只在本文件内部有效,而其他文件不可连接或引用该变量
野指针
核心:野指针就是指向无效地址的指针
,其他各种说法都是产生野指针(或者说是地址无效)的原因……
注意:上面所说的无效,即包括地址未分配、访问权限不足,也包括能访问但是不应该访问(比如指向的内存本来已释放但是未置指针为NULL,而后该地址处又被填充了其他内容,此时原指针虽然能访问此地址但是理论上是不应该访问的……)
产生野指针原因:野指针=> 待学习……
C语言参数压栈顺序
从右到左 => 属于ABI范畴了……
二、cpp中的关键字
static
1.修饰全局变量
作用域:此时即使是全局变量,但是也仅在当前文件内有效
;
存储区域:静态存储区(.text、.rodata、.data、.bss等编译时就已静态分配空间的区域,其中.rodata常被称为常量区)
;
生命周期:在整个程序运行期间一直存在;
初始化:未初始化的全局静态变量变量被自动初始化为0(.bss中)
;
2.修饰局部变量
作用域:它所在的函数/语句块内有效
,离开作用域后虽然不能修改,但是仍然存在并未销毁;
存储区域:静态存储区(.text、.rodata、.data、.bss等编译时就已静态分配空间的区域);
生命周期:在整个程序运行期间一直存在;
初始化:未初始化的局部静态变量变量被自动初始化为0(.bss中)
;
3.修饰函数
作用域:仅当前文件内可见;
存储区域:静态存储区(.text、.rodata、.data、.bss等编译时就已静态分配空间的区域),具体而言是在代码段.text中
;
生命周期:在整个程序运行期间一直存在;
4.修饰类的成员变量
用于保证类的多个对象共享一个成员
5.修饰类的成员函数
用于保证类的多个对象共享一个函数
volatile(todo)
explicit
- 概述
有时可以将构造函数用作自动类型转换函数 C++ 中explicit的作用=> 被explicit关键字修饰的类构造函数,不能进行自动地隐式类型转换,只能显式地进行类型转换 - 注意
只有一个参数的构造函数,或者构造函数有n个参数,但有n-1个参数提供了默认值,这样的情况才能进行类型转换。
public、protected、private
-
private,public,protected的访问范围
public:可以被该类的成员函数、友元的成员函数、子类的成员函数访问,也可以被自己类的对象访问
protected:可以被该类中的成员函数访问、子类中的成员函数访问、友元中的成员函数访问,但是不能被该类的对象访问
private:只能由该类的成员函数、友元的成员函数访问,不能被其他类的成员函数访问,即使是该类的对象也不能直接访问,子类也不能访问
关键:类对象能否访问、子类能否访问 -
类继承之后,方法属性的变化
1.使用public继承,父类中的方法不发生变化
2.使用protected继承,父类中的public和protected方法在子类中变为protected,private属性不变
3.使用private继承,父类的所有方法在子类中都变为private;
(即基类中所有限制更少的属性变为指定的这个属性即可)
const(待补充)
-
修饰成员函数
防止成员函数修改被调用对象的值
,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为 const 成员函数 =>尽量将所有的不需要改变对象(参数)内容的函数都作为 const 成员函数
,使用方法:int get_cm() const{ ...... }
注意:const 关键字不能与 static 关键字同时使用,因为 static 关键字修饰静态成员函数,静态成员函数不含有 this 指针,即不能实例化,const 成员函数必须具体到某一实例
-
修饰返回值
1.const 修饰内置类型的返回值:修饰与不修饰返回值作用一样;
2.const 修饰自定义类型的作为返回值:此时返回的值不能作为左值使用
,既不能被赋值,也不能被修改;
3.const 修饰返回的指针或者引用:返回的指针不能被修改 -
修饰常量
如果修饰的不是指针,const放在类型前/后都可以
int const x=1; const int x=1; // 二者等价
-
区分:const指针和指向常量的指针
typeid
- 概述
typeid 运算符用来获取一个表达式的类型信息;
基本类型:类型信息主要是指数据的类型(int、float、double);
类类型:类型信息是指对象所属的类、所包含的成员、所在的继承关系等;
返回值:typeid 会把获取到的类型信息保存到一个 type_info 类型的对象里面……
nullptr
- 概述
在C语言中,我们常常用NULL作为指针变量的初始值;
而在C++中,最好使用nullptr
参考:为什么建议你用nullptr而不是NULL?
extern “C”
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码
。加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
所以当C++中调用C库提供的函数时,若没有使用extern "C",那么在链接阶段,将会找不到期望的函数符号
。比如对于函数void foo(int a, int b);C++期望链接时找到的函数符号是_foo_int_int,而C库实际提供的函数符号是foo
更多参考:extern “C”的作用详解
三、面向对象
struct与class的区别
共同点:都可用于定义类、都可继承
区别:struct的默认权限是public,而class的默认权限是private; struct不能用于定义模板
,而class可以;
更多参考:C++面试题一—Struct和Class的区别
虚函数与多态专题
virtual成员函数
- 概述
虚函数是指一个类中你希望重写的成员函数
,当你用一个基类指针或引用
(但不能直接子类对象赋值给父类对象,这回导致切割!!!) 指向一个继承类对象的时候,调用一个虚函数时, 实际调用的是继承类的版本. - 注意
1.可以在基类中实现!
2.如果基类没有提供实现,子类必须实现它
pure-virtual函数(抽象函数)
- 概述
特殊的virtual函数,在基类中不能有实现
=> 而普通的virtual可以在基类中实现;
凡是含有纯虚函数的类叫做抽象类,这种类不能声明对象
;
抽象类必须被继承,然后实现纯虚函数后,才能实例化……
继承时的virtual关键字
如图,若不使用虚拟继承,则D中同时存在A的两个副本;
使用virtual继承能保证在派生类中只保留一份间接基类的成员
;
为什么基类的析构函数必须是虚函数?默认析构函数不是虚函数?
1.基类的析构函数必须是虚函数
这样可以保证在多态情况下,new一个子类,然后用基类指针指向子类对象后,释放基类指针时可以释放掉子类的空间,从而防止内存泄漏。因为这是多态情况,所以调用基类方法需要先调用子类方法,从而能保证释放掉子类对象 => 这是《Effective C++》的条款7
2.默认析构函数不是虚函数
虚函数需要额外的虚函数表vtbl和虚表指针vptr
(参考《c+=对象模型》),占用额外的内存。对于不是基类的类来说,没有必要设置虚析构,所以默认的析构函数不是虚函数
类的静态函数与虚函数的区别
静态函数在编译时就已经确定了运行时机;
虚函数是在运行时动态绑定
,且虚函数表机制导致调用时增加一次内存开销
虚函数与多态
1.最常见的两种多态
静态多态:主要指重载,在偏移的时候就已经确定;
动态多态:主要指重写,通过虚函数机制实现,在运行期间动态绑定
2.虚函数机制
对于有虚函数的类,对象的最开始部分是虚表指针vptr
,这个指针指向虚函数表vtbl
,vtbl中存放了各虚函数的地址。虚函数实际存放在代码段.text中
;
子类继承父类时也会继承其虚函数表,当子类重写父类中虚函数的时候,会将继承到的虚函数表中的地址替换为重新写的函数地址
;
=> 解释:子类继承父类的虚函数表,指的是对父类虚函数表的拷贝
,而不是共享。若子类没有重写父类虚函数,那么这个拷贝的虚函数表中各项指向的仍然是父类中定义的函数;若子类重写了虚函数,那么这个拷贝的虚函数表中相应地址替换为新函数地址
更多参考:C++ 虚函数表及多态内部原理详解(一)
注意:使用虚函数机制,会增加访问内存的开销,降低效率
多态原理分析(较难,待完成)
动态绑定
动态绑定是指在执行期间(非编译期)判断所引用对象的实际类型,根据其实际的类型调用其相应的方法
。程序运行过程中,把函数(或过程)调用与响应调用所需要的代码相结合的过程称为动态绑定。
举例分析
class Base1{
public:
virtual ~Base1(){}
virtual void func(){
cout<<"i am func in base1"<<endl;
}
};
class Base2{
public:
virtual ~Base2(){}
virtual void func(){
cout<<"i am func in base2"<<endl;
}
};
class Derived:public Base1,public Base2{
public:
void func(){
cout<<"i am func in derived"<<endl;
}
};
int main(){
Derived* d=new Derived();
Derived* pd=d;
Base1* pb1=d;
pb1->func(); // i am func in base1
Base2* pb2=d;
pb2->func(); // i am func in derived
printf("d point to addr:%x\n",pd); // d point to addr:dc5c20
printf("pb1 point to addr:%x\n",pb1); // pb1 point to addr:dc5c20
printf("pb2 point to addr:%x\n",pb2); // pb2 point to addr:dc5c20
return 0;
}
为什么这里的结果与多重继承下,不同基类指针指向同一子类对象的地址问题——腾讯一笔试题 不同?
部分可参考:基类与派生类,父类指针指向子类对象
C++ 虚函数与动态绑定原理剖析
多重继承下,不同基类指针指向同一子类对象的地址问题——腾讯一笔试题
inline成员函数
在程序调用这些inline成员函数时,并不是真正地执行函数的调用过程,而是把函数代码嵌入程序的调用点,这样可以大大减少调用成员函数的时间开销。
C++要求对一般的内置函数用关键字inline声明,但对类内定义的成员函数,可以省略inline,因为这些成员函数已被隐含地指定为内置函数
,如下:
class Student //声明学生类
{
private: //声明以下为私有的
int number; //学号
char name[10];//姓名
char sex; //性别
public: //声明以下为共有的
inline void print_info() //在类外定义打印函数
{
cout<<number<<endl;
cout<<name<<endl;
cout<<sex<<endl;
}
};
运算符重载
可在类内重载
;也可在类外重载
=> 用法一样,但是写法稍微不同
参考C++运算符重载
三种继承方式public、protected、private
- 概述
继承方式限定了基类成员在派生类中的访问权限
,包括 public(公有的)、private(私有的)和 protected(受保护的)。此项是可选项,如果不写,默认为 private
(成员变量和成员函数默认也是 private) - public
1.基类中所有 public 成员在派生类中为 public 属性;
2.基类中所有 protected 成员在派生类中为 protected 属性;
3.基类中所有 private 成员在派生类中不能使用。 - protected
1.基类中的所有 public 成员在派生类中为 protected 属性;
2.基类中的所有 protected 成员在派生类中为 protected 属性;
3.基类中的所有 private 成员在派生类中不能使用。 - private
1.基类中的所有 public 成员在派生类中均为 private 属性;
2.基类中的所有 protected 成员在派生类中均为 private 属性;
3.基类中的所有 private 成员在派生类中不能使用。 - 小结
继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的
自定义的构造/析构函数与合成的构造/析构函数(易忘)
1.若用户未自定义
如果用户没有自定义构造/析构函数,编译器会自动生成一个缺省的构造/析构函数
,它不进行任何操作,所以许多简单的类中没有显示定义构造/析构函数,但并不意味着没有构造/析构函数
2.若用户自定义了
即使用户自定义了,也可能在需要的时候再次合成一个构造/析构函数
(可能,不是一定),此时会先调用自定义的,再调用合成的
类析构的顺序(重要)
1.派生类的析构函数;
2.对象成员的析构函数;
3.基类的析构函数;
=> 这个顺序很重要,恰好与类的构造顺序相反
四、STL
STL基本组成部分
六大部分:容器、迭代器、仿函数、算法、分配器、配接器
补充:配接器更多的是一种设计模式(适配器模式)。即将一个class的接口转换为另一个class的接口,使原本因接口不兼容而不能合作的classes,可以一起运作。比较典型的就是通过deque实现stack和queue。
map与set的区别
相同点:底层都是红黑树实现
区别:
1.map中存放的是key-val; 而set中每个元素仅包含一个key;
2.map的迭代器允许修改val但不允许修改key;而set的迭代器是const的,不允许修改元素的值;
3.map支持下标操作;set不支持下标操作;
……
STL容器类型
主要分为序列式容器
(vector、list、deque、stack、queue)和关联式容器
(set、map……),
更多参考:STL容器类型
STL迭代器删除元素
对于序列式容器中的vector、deque:使用erase(iterator)后,后面每个元素原来的迭代器都会失效(因为所有元素都向前移动了一个位置)。erase()会返回下一个有效的迭代器;
对于序列式容器中的list:由于非连续内存分配,后续元素原来的迭代器不会失效;
对于关联式容器:不会影响后续元素原来的迭代器……
指针与迭代器的区别
迭代器不是指针,是类模板
。迭代器是一个可遍历STL容器内部全部或部分元素的对象。它封装了原生指针,并重载了指针的一些操作符(->、*、++、–等),所以表现得像指针。
resize和reserve的区别
简而言之,resize()会构造出新的元素,而reserve()只是为元素预留出空间而已
更多参考:vector.resize 与 vector.reserve的区别
emplace_back
push_back()
:先向容器尾部添加一个右值元素(临时对象),然后调用构造函数构造出这个临时对象,最后调用移动构造函数将这个临时对象放入容器中并释放这个临时对象。
注:最后调用的不是拷贝构造函数,而是移动构造函数。因为需要释放临时对象,所以通过std::move进行移动构造,可以避免不必要的拷贝操作
emplace_back()
:在容器尾部添加一个元素,调用构造函数原地构造,不需要触发拷贝构造和移动构造。因此比push_back()更加高效。
五、杂七杂八
cpp中四种类型转换
1.static_cast
static_cast基本上拥有与C旧式转换相同的威力与意义,以及相同的限制
,它是与C-style最接近的一个转换;
使用情况概述:通常是数值数据类型转换(比如float->int);不执行运行时类型检查(安全性不如dynamic_cast);能用于多态向上转换
,向下则不一定能保证真确;
补充:任何编写程序时能够明确的类型转换都可以使用static_cast(static_cast不能转换掉底层const,volatile和__unaligned属性
)。由于不提供运行时的检查,所以叫static_cast
,因此,需要在编写程序时确认转换的安全性。
2.const_cast
const_cast用于改变表达式中的常量性(const)或易变性(volatile),以及__unaligned属性。
使用情况概述:最常用的是将const转换为非const;当然它其实还能去掉volatile性质
、改变__unaligned的心性质;
疑问:旧式的C转换如何做到const_cast同样效果呢?
3.dynamic_cast
dynamic_cast用于多态的安全向下转型
以及向上转型。
使用情况概述:用于多态类型向上/向下转型;会执行运行时检查;
4.reinterpret_cast
reinterpret_cast就是bit的简单重新解释,几乎什么都可以转
。
使用情况概述:尽量少使用
注:static_cast与const_cast的区别
在多重继承上可以体现二者的区别:
class A {
public:
int m_a;
};
class B {
public:
int m_b;
};
class C : public A, public B {};
int main(){
C c;
printf("%p, %p, %p", &c, reinterpret_cast<B*>(&c), static_cast <B*>(&c));
}
//打印结果: 0x7fff17de3820, 0x7fff17de3820, 0x7fff17de3824
C++通过指针来访问成员的过程,实际上是根据指针的类型,找到改类定义,并从中找到要访问的成员的地址偏移,然后从对象指针开始加上偏移,便得到了成员的地址,然后进行访问操作。所以类定义的作用之一在于,确定成员的内存偏移。
static_cast会在编译期将指针在类的内存空间内移动,并最终指向到你转换到的内存上
。在上面的例子中,当将C的指针转型为B型指针时,实际上将指针的数值修改了,指向了内存中B类的开头位置。当使用转换后的指针访问B的成员时,先到B类定义查到B的内存分布,然后进行指针便宜,访问。一切都是正确的。
reinterpret_cast并不会在转型是修改指针的值,而是告诉编译器,这个指针是某个类型的指针,仅此而已
。所以当使用此转型方式转换C的指针到B类型时,指针仍然指向原来的位置,即C的开头,也是A的开头。这时如果访问B*的成员m_b,那么先到B上查到m_b的地址偏移为一个整型的字节大小(一般是4),那么你实际是将指针偏移到了A类的m_a成员处,访问的是m_a的内容。这种错误是编译器不明白的,你的程序即将崩溃。
四个智能指针
-
作用
智能指针用于管理指针,避免内存泄漏 =>智能指针就是一个类,当超出类的作用域时,类自动调用析构函数,释放内存空间
,从而避免内存泄漏 -
1.auto_ptr
auto_ptr是C98中的智能指针,C11及以后已弃用。auto_ptr是独占式拥有,auto_ptr采用所有权模式,当它赋值拷贝后,所有权发生转移
,如下:auto_ptr<string> p1(new string("hello icg!")); auto_ptr<string> p2; p2=p1; cout<<*p2<<endl; cout<<*p1<<endl; // hello icg! // Segmentation fault
如图所示,当将p1赋给p2后,对象的所有权便属于p2,所以打印p1时报错! => 简言之,
auto_ptr不能共享
-
2. unique_tr
unique_ptr采用独占式拥有
。和auto_ptr一样采用所有权模式,是auto_ptr的替代
。它相对于auto_ptr优点在于:若所有权会发生转移,直接在编译期报错
,而auto_ptr仅在运行期报错!
可用于解决典型内存泄漏问题:以new创建对象后因为发生异常而忘记调用delete -
3.shared_ptr
shared_ptr采用共享式拥有
。共享式拥有就是既共享也拥有,这一点与unique_ptr有所区别。多个shared_ptr指针可以指向同一个对象,该对象会在最后一个引用被销毁时释放。shared_ptr解决了auto_ptr不能共享的问题
。示例:shared_ptr<string> p1(new string("hello icg!")); shared_ptr<string> p2; p2=p1; cout<<p1.use_count()<<endl; cout<<p2.use_count()<<endl; cout<<*p1<<endl; cout<<*p2<<endl; // 2 // 2 // hello icg! // hello icg!
=> 简言之,
shared_ptr共享且可以拥有
(由最后一个引用释放)
shared_ptr的应用:自动解除互斥锁
(待学习)、防范Cross-DLL问题
(对象在一个DLL中被创建,却在另一个DLL中被销毁) -
4.weak_ptr
weak_ptr仅共享但不拥有对象,它协助shared_ptr工作
。weak_ptr是不控制对象生命周期的指针,它指向shared_ptr管理的对象。weak_ptr的构造与释放并不会造成对象引用计数的变化(弱引用)
。
作用:解决shared_ptr相互引用时的死锁问题,如下:class B; class A{ public: shared_ptr<B> pb_; // 对象A将会引用一个对象B ~A(){ cout<<"A delete\n"<<endl; } }; class B{ public: shared_ptr<A> pa_; // 对象B将会引用一个对象A ~B(){ cout<<"B delete\n"<<endl; } }; void func(){ shared_ptr<A> pa(new A()); shared_ptr<B> pb(new B()); pa->pb_=pb; pb->pa_=pa; cout<<pa.use_count()<<endl; // 2 cout<<pb.use_count()<<endl; // 2 } int main(){ func(); ...... return 0; }
如上,两个对象形成了循环引用,pa、pb的引用计数都为2。当func退出时,pa、pb引用计数都减1,但是仍旧无法释放资源,造成内存泄漏(进程退出前这块虚拟内存都无法使用)
解法方法:将A中shared_ptr pb_; 改为weak_ptr pb_; func中设置pa->pb_=pb;时,对象B的引用计数仍然是1而不会增减(因为weak_ptr只是一个弱引用) => 所以func退出时能释放B,而B的释放导致A的引用计数减1(因为释放B时释放了成员shared_ptr pa_😉,于是对象A的引用计数变为1;func退出也意味着A的作用域结束,A的引用计数再减1,于是成功释放……
private成员除了对象的setter方法外就无法修改吗—no
- 概述
本质上访问控制应该只是编译期的事情,实际上在运行期仍然可以通过指针修改对象的私有成员
(只要知道对象的内存布局的话),如下:
输出结果:class Node{ private: int x; int y; public: Node(int x,int y){ this->x=x; this->y=y; } int getX(){ return this->x; } int getY(){ return this->y; } }; int main(){ Node node(1,2); // 定义并初始化node cout<<"node.y="<<node.getY()<<endl; Node* pNode=&node; int* mypNode=reinterpret_cast<int*>(pNode); *(mypNode+1)=99; // 这里尝试修改node的私有成员y cout<<"now node.y="<<node.getY()<<endl; return 0; }
node.y=2 now node.y=99
静态绑定和动态绑定(待深入)
-
静态类型和动态类型
静态类型:就是在代码中被声明时采用的类型 => 程序执行过程中不能改变;
动态类型:目前所指对象的类型 => 可在程序执行过程中改变(通常是由于赋值动作)。
动态绑定的实现
:C++ 静态绑定和动态绑定 -
注意事项
non-virtual函数是静态绑定的;
vitual函数是动态绑定的;
缺省参数值是静态绑定的 => 绝不重新定义继承而来的缺省参数值 !!!
指针与引用的区别
- 1.指针有自己的内存空间,而引用只是一个别名
指针有自己内存空间好理解;关于引用只是一个别名
,证明如下:
可以看到引用a、b取地址结果是相同的,这个地址其实就是x的内存地址;int x=100; int& a=x; int& b=x; printf("addr a:%x\t addr b:%x\n",&a,&b); // addr a:17107c4 addr b:17107c4
- 2.sizeof
对指针使用sizeof(),结果始终是指针的大小,32位下为4,64位下为8;
对引用使用sizeof(),结果是被引用对象的大小;
当然,之所以结果不同,这是由编译器规定的…… - 3.传参时是否需要解引用
指针:作为参数传递时需要解引用才能直接操作对象,否则只能使用指针的方式(->);
引用:可以直接操作 - 4.修改绑定对象
指针:能修改绑定对象,只要将指针的值修改为新对象的地址即可;
引用:不能修改绑定对象,对引用的修改,修改的只是其绑定的对象的内容,并不能将引用绑定到另一个对象上
如何定义常量,常量存储在哪里?
const 定义常量,常量必须初始化
;
存放位置
对于局部对象,常量存放在栈区;
对于全局对象,常量存放在全局/静态存储区区(.data、.bss等
);
对于字面值常量,存放在常量存储区(c++通常将.rodata称为常量区
);
RTTI(待深入)
运行时类型检查,最关键的是两个部分:dynamic_cast、typeid;
由于面向对象程序设计中多态性的要求,C++中的指针或引用(Reference)本身的类型,可能与它实际代表(指向或引用)的类型并不一致。有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就产生了运行时类型识别的要求。和Java相比,C++要想获得运行时类型信息,只能通过RTTI机制,并且C++最终生成的代码是直接与机器相关的。
VS中虚函数表的-1位置存放了指向type_info的指针,对于存在虚函数的类型,typeid和dynamic_cast都会去查询type_info
更多参考:C++中的RTTI机制解析
为什么编译时无法确定类型信息:待深入……
cpp如何处理返回值
编译器会在执行函数前生成一个临时变量,然后将这个变量的引用作为参数传递给函数
,返回值其实就是这个传入的临时变量,只是用户在cpp代码层面感知不到罢了
boost::function
function是一个函数对象的“容器”
,概念上像是C/C++中函数指针类型的泛化,是一种“智能函数指针”。它以对象的形式封装了原始的函数指针或函数对象,能够容纳任意符合函数签名的可调用对象。因此,它可以被用于回调机制,暂时保管函数或函数对象,在之后需要的时机在调用,使回调机制拥有更多弹性。举例:
#include <iostream>
#include "boost/function.hpp"
bool some_func(int i,double d) {
return i>d;
}
int main() {
boost::function<bool (int,double)> f; //bool指定返回类型,(xx,xx)指定参数类型
f=&some_func;
f(10,1.1);
}
thread affinity
如果你正在开发低延迟的网络应用,那应该对线程亲和性(Thread affinity)有所了解。线程亲和性能够强制使你的应用线程运行在特定的一个或多个cpu上
。通过这种方式,可以消除操作系统进行调度过程导致线程迁移所造成的影响。
参考自:线程亲和性(Thread Affinity)
class与类名之间的修饰符(待完善)
class CPPCONN_PUBLIC_FUNC MySQL_Connection : public sql::Connection {
public:
......
}
一般情况下,在class 和类名之间的则是导入导出符号。意思为函数或变量提供给其他PE文件使用,来实现动态链接的过程;
更多参考:新手请教:C++定义时,以class和类名中间的那个修饰符是什么意思?
感觉讨论的都是windows平台的?
友元类/友元函数
- 一个类 A 可以将另一个类 B 声明为自己的友元,类 B 的所有成员函数就都可以访问类 A 对象的私有成员
- 在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为“友元”,这样那些函数就成为该类的友元函数,在友元函数内部就可以访问该类对象的私有成员了
attribute((unused))
在C程序中,如果定义了一个静态函数,而没有去使用,编译时会有一个告警;
而使用attribute((unused))可以告诉编译器忽略此告警:
comparator 的返回值与排序顺序关系(重要)
返回值<0 => o1排在o2前面
返回值>0 => o2排在o1前面
返回值=0 => 谁排在前面都可以
或者理解为:返回值为正数, 就交换参数1和参数2的位置
cpp模板编译时多态
cpp的模板是编译时多态。所有的模板都是在编译时产生对应的代码,它没有面向对象中的虚表,无法实现动态多态。
成员函数末尾的const
举例:
class IndexIterator {
public:
bool operator==(const IndexIterator &itr) const { throw std::runtime_error("unimplemented"); }
bool operator!=(const IndexIterator &itr) const { throw std::runtime_error("unimplemented"); }
private:
// add your own private member variables here
};
const 指明:这个函数不会修改对象里的任何成员变量的值
函数返回引用类型(TODO)
暂时参考:C++之函数返回引用类型
六、现代C++新特性
右值引用(待深入)
1.左值右值
左值:具名对象,或者能取地址的表达式。左值既能出现在等号的左边,右能出现在等号的右边;
右值:匿名对象,或者不能取地址的表达式
。右值只能出现在等号右边;
举例:
int a=3; // a是左值,3是右值
int b=4; // b是左值,4是右值
int c=a+b; // a、b、c都是左值,但是a+b是右值!!!
2.右值引用
c++98/03只能对左值添加引用,而不能对右值添加引用
,如下:
int num = 10;
int &b = num; //正确
int &c = 10; //错误
c11开始支持对右值添加引用,使用符号&&
;
和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化;
右值引用还支持对右值进行修改
:
int num = 10;
//int && a = num; // 错误,右值引用不能初始化为左值
int && a = 10; // c11下正确
a = 100; //右值引用支持修改右值
cout << a << endl;
3.右值引用的作用
a.消除两个对象交互时不必要的对象拷贝,节省运算存储资源;
b.能够更简洁明确地定义泛型函数;
…… 待深入,举个例子?
移动语义(std::move)和完美转发(std::forward)(待完成)
std::move
std::move是为了转移所有权,将快要销毁的对象转移给其他变量,这样可以继续使用这个对象,而不必再创建一个一样的对象
。省去了创建新的一样内容的对象,也就提高了性能。对于某些资源来说,可以改变所有者,但是只能有一份,move也解决这样的对象的管理问题
参考:什么是move
Python,Java等没有move概念,因为在这些有GC的语言里面,赋值既具有"move"的性质,又具有copy的性质——实际是将引用复制给另外一个变量
。
std::forward
七、语言之外
大端与小端
- 大端模式
大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中;这和我们的阅读习惯一致
=> 助记:大方端正地阅读(大阅
) - 小端模式
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中
RAII
RAII,也称为“资源获取就是初始化”,是c++等编程语言常用的管理资源、避免内存泄露的方法。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。
core dump
在linux下开发时,如果程序突然崩溃了,也没有任何日志。这时可以查看core文件。从core文件中分析原因,通过gdb看出程序挂在哪里,分析前后的变量,找出问题的原因 => 这一行为就叫核心转储