C++基础知识整理

  • 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等复杂操作的内联函数
  • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。

优缺点

优点

  1. 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
  2. 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
  3. 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
  4. 内联函数在运行时可调试,而宏定义不可以。

缺点

 

  1. 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  2. inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
  3. 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。

虚函数可以是内联函数吗?

 

 

  1. 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
  2. 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
  3. 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释放时,只调用了父类的析构函数,没有调用子类的析构函数。不申请内存不会出现这样的问题。

 

  • 虚函数、纯虚函数

类里面如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,主要目的是为了实现多态。

纯虚函数只是一个接口,是个函数的声明,它要留到子类里去实现。

虚函数在子类里面也是可以不重载的,但纯虚函数必须在子类去实现。

虚函数的类用于“实作继承”,继承接口的同时也继承了父类的实现。当然也可以在子类中自己实现。纯虚函数关注的是接口的统一性,由子类去实现。

抽象类:带纯虚函数的类,这种类不能直接生成对象,而只有被继承,重写其纯虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。

虚基类:也是带有纯虚函数的类,它如果被继承,那么子类就必须实现虚基类里面的所有纯虚函数,其子类不能是抽象类。

 

  • 虚函数指针、虚函数表

虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。

虚函数表:在程序只读数据段,存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。

https://blog.twofei.com/496/

 

  • 虚继承、虚函数

相同之处:都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)

不同之处:

虚继承:虚基类依旧存在继承类中,只占用存储空间

虚基类表存储的是虚基类相对直接继承类的偏移

虚函数:虚函数不占用存储空间

虚函数表存储的是虚函数指针

 

  • 抽象类、接口类、聚合类

抽象类:含有纯虚函数的类

接口类:仅含有纯虚函数的抽象类

聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法。

  1. 所有成员都是public
  2. 没有定义任何构造函数
  3. 没有类内初始化
  4. 没有基类、也没有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++

  1. 尽量使用编译器而不是预处理(使用const代替#define),预处理的字段编译时如果报错,报错信息会很难去跟踪,所以,尽量使用const来代替。
  2. 尽可能使用const
  3. 将多态基类的析构函数声明为virtual(如果class带有任何virtual函数,它就应该拥有一个virtual析构函数)
  4. 成对使用new和delete时要采取相同形式(new中使用[]则delete[],new中不使用[]则delete)
  5. 将成员变量声明为private(为了封装、一致性、对其读写精确控制等)
  6. 不要轻忽编译器的警告

 

  • 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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值