-
const
//指针
char greeting[] = "hello";
char *p1 = greeting; //指针变量,指向字符数组变量
const char *p2 = greeting; //指针变量,指向字符数组常量
char *const p3 = greeting; //常量指针,指向字符数组变量
const char * const p4 = greeting; //常量指针,指向字符数组常量
//函数返回值
const int * function6(); //返回一个指向常量的指针变量,使用:const int *p = function6();
int * const function7(); //返回一个指向变量的常量指针,使用:int * const p = function7();
-
static
1、修饰普通变量,变量存储在静态区,只会被初始化一次,默认值为0;
2、修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定为 static。
3、修饰成员变量,该类只保存一个该变量,可以通过类直接访问,在类中声明,在类外初始化。
4、修饰成员函数,无需生成对象可以直接访问,static函数内不能访问非静态成员。
备注:类在实例化的时候,是通过new关键字来进行的,new时会默认提供一个隐藏的this指针,该指针的作用是用来访问实例对象的成员变量的。
https://blog.csdn.net/xiadeliang1111/article/details/90737007
-
inline内联函数
特征
- 相当于把内联函数里面的内容写在调用内联函数处
- 相当于不用执行进行函数的步骤,直接执行函数体
- 相当于宏,却比宏多了类型检查,真正具有函数特性
- 编译器一般不内联包含循环、递归、switch等复杂操作的内联函数
- 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。
优缺点
优点
- 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
- 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
- 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
- 内联函数在运行时可调试,而宏定义不可以。
缺点
- 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
- inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
- 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。
虚函数可以是内联函数吗?
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
- inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
虚函数的内联使用:
#include <iostream>
using namespace std;
class Base
{
public:
inline virtual void who()
{
cout << "I am Base\n";
}
virtual ~Base() {}
};
class Derived : public Base
{
public:
inline void who() // 不写inline时隐式内联
{
cout << "I am Derived\n";
}
};
int main()
{
// 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,
//编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。
Base b;
b.who();
// 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。
Base *ptr = new Derived();
ptr->who();
// 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,
//会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
delete ptr;
ptr = nullptr;
return 0;
}
-
volatile
volatile int i = 10;
- volatile关键字是一种类型修饰符,声明的变量有可能被更改,告知编译器不应对这样的对象进行优化。
- volatile关键字声明的变量,每次访问都必须从内存中取值,没有被volatile修饰的变量,可能由于编译器的优化,从CPU中取值。
-
assert()
断言,是宏,并非函数。头文件<assert.h>,作用是如果返回错误,则终止程序,可以通过定义NDEBUG来关闭assert。主要用于调试,避免显而易见的错误。
#define NDEBUG // 加上这行,则 assert 不可用
#include <assert.h>
assert( p != NULL ); // assert 不可用
-
extern "C"
被extren "C" 修饰的变量和函数是按照C语言的方式编译和链接的。
extern "C"的作用是让C++编译器将extern "C"声明的代码当作C语言代码处理,可以避免C++因符号修饰导致代码不能和C语言库中的符合进行链接的问题。
#ifdef __cplusplus //C++中默认带有该宏
extern "C" {
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif
-
explicit(显示)关键字
explicit修饰构造函数时,可以防止隐式转换和复制初始化
explicit修饰转换函数时,可以防止隐式转换,但按语境转换除外
主要就是防止隐式转换的情况发生。
struct A
{
A(int) { }
operator bool() const { return true; }
};
struct B
{
explicit B(int) {}
explicit operator bool() const { return true; }
};
void doA(A a) {}
void doB(B b) {}
int main()
{
A a1(1); // OK:直接初始化
A a2 = 1; // OK:复制初始化
A a3{ 1 }; // OK:直接列表初始化
A a4 = { 1 }; // OK:复制列表初始化
A a5 = (A)1; // OK:允许 static_cast 的显式转换
doA(1); // OK:允许从 int 到 A 的隐式转换
if (a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a6(a1); // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a7 = a1; // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
bool a8 = static_cast<bool>(a1); // OK :static_cast 进行直接初始化
B b1(1); // OK:直接初始化
B b2 = 1; // 错误:被 explicit 修饰构造函数的对象不可以复制初始化
B b3{ 1 }; // OK:直接列表初始化
B b4 = { 1 }; // 错误:被 explicit 修饰构造函数的对象不可以复制列表初始化
B b5 = (B)1; // OK:允许 static_cast 的显式转换
doB(1); // 错误:被 explicit 修饰构造函数的对象不可以从 int 到 B 的隐式转换
if (b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
bool b6(b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换
bool b7 = b1; // 错误:被 explicit 修饰转换函数 B::operator bool() 的对象不可以隐式转换
bool b8 = static_cast<bool>(b1); // OK:static_cast 进行直接初始化
return 0;
}
-
friend友元类和友元函数
- 能访问私有成员
- 破坏了封装性
- 友元关系不可传递
- 友元关系的单向性
- 友元声明的形式及数量不受限制
类的私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接的访问。提供了封装性,但也带来了书写的麻烦,于是友元函数就出现了。
-
友元函数
声明在类里,定义在类外的普通函数,不是类的成员函数,但可以直接访问类的私有成员。
class A
{
public:
void getA(){cout<<a<<endl;}
private:
int a;
friend void get_a(A A1);
};
void get_a(A A1)
{
cout<<A1.a<<endl;
}
比如这里的函数get_a,它就是A的友元函数,可以直接访问类A的私有成员。
-
友元类
一个类A可以将另一个类B声明为自己的友元,类B的所有成员函数就都可以访问类A对象的私有成员。
class A
{
public:
void getA(){cout<<a<<endl;}
private:
int a;
friend class B;
};
class B
{
public:
void getA()
{
A A1;
A1.a = 5;
A1.getA();
}
};
这里类B的成员函数就都可以访问类A的私有成员。
-
using指示与using声明
尽量少使用using指示:
using namespace std;
应该多使用using声明:
int x;
std::cin >> x ;
std::cout << x << std::endl;
或者
using std::cin;
using std::cout;
using std::endl;
int x;
cin >> x;
cout << x << endl;
-
::范围解析运算符
分类
1、全局作用域符(::name):用于类型名称前,表示作用域为全局命名空间。
2、类作用域符(class::name):表示指定类型的作用域范围时具体某个类的。
3、命名空间作用域符(namespace::name):表示指定类型的作用域范围是具体某个命名空间。
-
enum枚举类型
限定作用域的枚举类型
enum class open_modes { input, output, append };
不限定作用域的枚举类型
enum color { red, yellow, green };
enum { floatPrec = 6, doublePrec = 10 };
#include <iostream>
using namespace std;
enum CMD_RESULT{R_TRUE, R_FALSE, R_UNKNOW};
//enum {R_TRUE, R_FALSE, R_UNKNOW};
int main()
{
int result = 2;
switch(result)
{
case R_TRUE:cout<<"true"<<endl;break;
case R_FALSE:cout<<"false"<<endl;break;
case R_UNKNOW:cout<<"unhnow"<<endl;break;
}
CMD_RESULT cmd;
cout<<cmd<<endl;
return 0;
}
-
面向对象
面向对象三大特征——封装、继承、多态
-
重载、覆盖、隐藏
-
函数重载:函数名相同,参数个数、类型、顺序不同,不关注函数返回类型
#include <iostream>
#include <string>
using namespace std;
class Father
{
public:
int fun(int a, int b, int c){cout<<"fun 1"<<endl;}
int fun(int a, int b){cout<<"fun 2"<<endl;}
int fun(string a, string b){cout<<"fun 3"<<endl;}
};
int main()
{
Father father;
father.fun(1,2,3);
father.fun(1,2);
father.fun("aa", "bb");
return 0;
}
结果:
fun 1
fun 2
fun 3
-
函数覆盖(虚函数,多态接口的实现)
1、子类和父类中函数名、参数、返回类型都相同
2、父类的函数是虚函数
子类调用时,会调用子类的函数
class Father
{
public:
virtual void fun(){cout<<"father"<<endl;}
};
class Son:public Father
{
public:
void fun(){cout<<"son"<<endl;}
};
int main()
{
Father *father = new Father();
father->fun(); //father
delete father;
Father *son = new Son();
son->fun(); //son
delete son;
return 0;
}
-
函数隐藏
定义了子类的对象,调用了子类的函数
子类的函数与父类函数名称相同,但是参数不同,父类函数被隐藏
子类函数与父类函数的名称相同,参数也相同,但是父类函数不是虚函数,父类函数被隐藏
class Father
{
public:
void fun(){cout<<"father"<<endl;}
};
class Son:public Father
{
public:
void fun(){cout<<"son"<<endl;}
};
int main()
{
Father *father = new Father();
father->fun(); //father
delete father;
Son *son = new Son();
son->fun(); //son
delete son;
return 0;
}
定义子类对象,调用子类函数,父类函数被隐藏。
-
封装
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
public成员:可以被任意实体访问
protected成员:只允许被子类及本类的成员函数访问
private成员:只允许被本类的成员函数访问
-
虚析构函数
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
class Shape
{
public:
Shape(); // 构造函数不能是虚函数
virtual double calcArea();
virtual ~Shape(); // 虚析构函数
};
class Circle : public Shape // 圆形类
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
shape1->calcArea();
delete shape1; // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。
shape1 = NULL;
return 0;
}
这里父类对象指针指向了堆上申请的子类对象,需要将父类对象的析构函数定义成虚析构函数,否则delete释放时,只调用了父类的析构函数,没有调用子类的析构函数。不申请内存不会出现这样的问题。
-
虚函数、纯虚函数
类里面如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,主要目的是为了实现多态。
纯虚函数只是一个接口,是个函数的声明,它要留到子类里去实现。
虚函数在子类里面也是可以不重载的,但纯虚函数必须在子类去实现。
虚函数的类用于“实作继承”,继承接口的同时也继承了父类的实现。当然也可以在子类中自己实现。纯虚函数关注的是接口的统一性,由子类去实现。
抽象类:带纯虚函数的类,这种类不能直接生成对象,而只有被继承,重写其纯虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。
虚基类:也是带有纯虚函数的类,它如果被继承,那么子类就必须实现虚基类里面的所有纯虚函数,其子类不能是抽象类。
-
虚函数指针、虚函数表
虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。
虚函数表:在程序只读数据段,存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。
-
虚继承、虚函数
相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
不同之处:
虚继承:虚基类依旧存在继承类中,只占用存储空间
虚基类表存储的是虚基类相对直接继承类的偏移
虚函数:虚函数不占用存储空间
虚函数表存储的是虚函数指针
-
抽象类、接口类、聚合类
抽象类:含有纯虚函数的类
接口类:仅含有纯虚函数的抽象类
聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法。
- 所有成员都是public
- 没有定义任何构造函数
- 没有类内初始化
- 没有基类、也没有virtual函数
-
malloc、free使用
char *str = (char*) malloc(100);
assert(str != nullptr);
free(p);
p = nullptr;
-
new、delete使用
1、new/new[]:完成两件事,先底层调用malloc分配了内存,然后调用构造函数(创建对象)
2、delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用free释放空间
3、new在申请内存时会自动计算所需字节数,而malloc则需我们自己申请内存空间的字节数。
T* t = new T(); // 先内存分配 ,再构造函数
delete t; // 先析构函数,再内存释放
return 0;
-
new/delete, new[]/delete[]用法
int *p1 = new int; //动态分配4个字节(1个int)空间的数据
int *p2 = new int(3); //动态分配4个字节(1个int)的空间并初始化为3
int *p3 = new int[3]; //动态分配12个字节(3个int)的空间
delete p1;
delete p2;
delete[] p3;
new/delete动态管理对象,new[]/delete[]动态管理对象数组
class Base{};
Base *p = new Base[10]; //调用10次构造函数
delete[] p; //调用10次析构函数
所以,malloc/free,new/delete,new[]/delete[]尽量各自搭配使用。
https://www.cnblogs.com/tp-16b/p/8684298.html
-
如何定义一个只能在堆上(栈上)生成对象的类?
https://www.nowcoder.com/questionTerminal/0a584aa13f804f3ea72b442a065a7618
只能在堆上
方法:将析构函数设置为私有或保护型
原因:C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能再栈上创建对象。
class Base
{
protected:
Base(){}
~Base(){}
public:
static Base* create()
{
return new Base();
}
void destory()
{
delete this;
}
};
int main()
{
Base *p = Base::create();
p->create();
p->destory();
return 0;
}
只能在栈上生成对象
方法:将new和delete重载为私有
原因:在堆上生成对象,使用new关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将new操作设置为私有,那么第一阶段就无法完成,就不能在堆上生成对象。
class Base
{
private:
void *operator new(size_t t){}
void operator delete(void *ptr){}
public:
Base(){};
~Base(){};
};
int main()
{
Base b;
return 0;
}
-
Effective C++
- 尽量使用编译器而不是预处理(使用const代替#define),预处理的字段编译时如果报错,报错信息会很难去跟踪,所以,尽量使用const来代替。
- 尽可能使用const
- 将多态基类的析构函数声明为virtual(如果class带有任何virtual函数,它就应该拥有一个virtual析构函数)
- 成对使用new和delete时要采取相同形式(new中使用[]则delete[],new中不使用[]则delete)
- 将成员变量声明为private(为了封装、一致性、对其读写精确控制等)
- 不要轻忽编译器的警告
-
Google C++ Style Guide 图
https://blog.csdn.net/voidccc/article/details/37599203
参考:https://github.com/huihut/interview
https://www.yuque.com/huihut/interview/readme#0zzhai