前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++ 万字长文第一篇---拿下字节面试

C++ 万字长文第一篇---拿下字节面试

作者头像
syy
发布2020-08-13 14:46:50
1.5K0
发布2020-08-13 14:46:50
举报
文章被收录于专栏:多选参数多选参数

关键字的作用

静态变量的初始化

代码语言:javascript
复制
int main() {
    int initNum = 3;
    for (int i=1; i<=5; i++) {
        static int n1 = initNum;
		n1++;
        printf("%d\n", n1);
    }
    return 0;
}
/*
4
5
6
7
8
*/

int main() {
    int initNum = 3;
    for (int i=1; i<=5; i++) {
        static int n1 = initNum;
		{
			int *p = &n1;
			p++;
			*p = 0;
		}
		n1++;
        printf("%d\n", n1);
    }
    return 0;
}
/*
4
4
4
4
4
*/

函数指针

在编译过程中,每一个函数都有一个入口地址,而函数指针就是指向该入口地址的指针。

代码语言:javascript
复制
#include<iostream>
using namespace std;

void fun1(int x) {
	cout << x << endl;
}

void fun2(int x) {
	cout << x+x <<endl;
}

int main() {
	void (*pf)(int);
	pf = fun1;
	pf(222);
	pf = fun2;
	pf(222);
}

多态性和虚函数(virtual)

  1. 静态多态主要表现为重载,在编译时就确定了。
  2. 动态多态的基础是虚函数机制,虚函数的作用是实现动态多态,在运行期间动态绑定,决定了派生类调用哪个函数。
代码语言:javascript
复制
#include<iostream>
using namespace std;

class Shape {
	public:
        void show() {   //  未定义为虚函数
			cout << "Shape::show()" << endl;
        }
		void virtual show() {   //  定义为虚函数
			cout << "Shape::show()" << endl;
		}
};

class Line : public Shape {
	public:
		void show() {
			cout << "Line::show()" << endl;
		}
};

class Point : public Shape {
	public:
		void show() {
			cout << "Point::show()" << endl;
		}
};

int main() {
	Shape *pt;
	pt = new Line();
	pt->show();
	pt = new Point();
	pt->show();
	return 0;
}
/*
未定义为虚函数时输出结果
Shape::show()
Shape::show()

定义为虚函数时输出结果
Line::show()
Point::show()
*/

纯虚函数

有些情况下,基类生成的对象是不合理的,比如动物可以派生出狮子、孔雀等,这些派生类显然存在着较大的差异。那么可以让基类定义一个函数,并不给出具体的操作内容,让派生类在继承的时候在给出具体的操作,这样的函数被称为纯虚函数。含有纯虚函数的类成为抽象类,抽象类不能声明对象,只能用于其他类的继承。 纯虚函数的定义方法为:

代码语言:javascript
复制
void ReturnType Function() = 0;

子类可以不重写虚函数,但一定要重写纯虚函数。

静态函数和虚函数

静态函数在编译时就确定了调用它的时机,而虚函数在运行时动态绑定,虚函数由于用到了虚函数表和虚函数虚函数指针,会增加内存使用。

构造函数和析构函数

  1. 构造函数在每次创建对象的时候调用,函数名称和类名相同,无返回类型,构造函数可以为类初始化某些成员。
  2. 析构函数在每次删除对象的时候调用,函数名称和类名相同,但在前面加了一个
\sim

符号,同样无返回类型。若对象在调用过程中用

new

动态分配了内存,可以在析构函数中写

delete

语句统一释放内存。

  1. 如果用户没有写析构函数,编译系统会自动生成默认析构函数。
  2. 假设存在继承:孙类继承父类,父类继承爷类
    1. 孙类构造过程:爷类 -> 父类 -> 孙类
    2. 孙类析构过程:孙类 -> 父类 -> 爷类

析构函数和虚函数

  1. 可能作为继承父类的析构函数需要设置成虚函数,这样可以保证当一个基类指针指向其子类对象并释放基类指针的时候,可以及时释放掉子类的空间。
  2. 虚函数需要额外的虚函数表和虚函数指针,占用额外的内存,所以不会作为继承父类的析构函数不用设置成虚函数,否则会浪费内存。

C

++默认的析构函数不是虚函数,只要当其作为父类的时候,才会设置为虚函数。

重载、重写(覆盖)、隐藏

可以通过 attribute 关键字,声明 constructordestructor 来实现。

代码语言:javascript
复制
#include<iostream>
using namespace std;

__attribute((constructor)) void before_main() {
	cout << __FUNCTION__ << endl;
}

__attribute((destructor)) void after_main() {
	cout << __FUNCTION__ << endl;
}

int main() {
	cout << __FUNCTION__ << endl;
    return 0;
}

虚函数表

在有虚函数的类中,存在一个虚函数指针,该指针指向一张虚函数表,当子类继承基类的时候,也会继承其虚函数表。当子类重写基类中的虚函数时,会将虚函数表中的地址替换成重写的函数地址。

char* 和 char[] 的区别

代码语言:javascript
复制
char *s1 = "abc";
char s2[] = "abc"

C
代码语言:javascript
复制
    int x = 10;
    const int *a = &x;
    int* const b = &x;

为什么函数参数入栈顺序从右到左

为了支持 不定长参数函数

代码语言:javascript
复制
int add(int num, ...) {
	va_list valist;
	int sum = 0;
    int i;
    va_start(valist, num);
    for (i = 0; i < num; i++) {
        sum += va_arg(valist, int);
    }
    va_end(valist);
    return sum;
}

C98
C98
代码语言:javascript
复制
enum color{red, green, blue, yellow};
enum color2{red, green};    // ERROR,因为 red 和 green 已经在 color 中定义过了
auto x = red;   //OK,因为 red 没有限定作用域
auto y = color::red;   //OK
C11
代码语言:javascript
复制
enum struct color{red, green, blue, yellow};
enum struct color2{red, green}; // OK,red 和 green 在不同作用域内
auto x = red;   // ERROR,red 没有指定作用域
auto y = color::red;

强类型转换的优点在于

  • 限定作用域的枚举类型将名字空间污染降低
  • 限定作用域的枚举类型是强类型的,无法通过隐式转换到其他类型,而不限定的枚举类型可以自动转换为整形

宏定义和枚举的区别

  1. 枚举是一种实体,占内存。宏定义是一种表达式,不占内存。
  2. 枚举在编译阶段进行处理,宏定义在与编译阶段就完成了文本替换。

空类

隐式类型转换

  1. 表达式中,低精度类型向高精度类型发生转换。
  2. 条件语句中,非布尔类型向布尔类型发生转换。
  3. 初始化语句中,初始值向变量类型发生转换。
  4. 赋值语句中,右侧运算对象向左侧运算对象发生转换。
  5. 可以用 单个形参 来调用的构造函数定义了从 形参类型 到 该类类型 的一个隐式转换。注意 单个形参 并不是只有一个形参,可以有多个形参,但其他形参要有默认实参。
代码语言:javascript
复制
#include<iostream>
using namespace std;

class Node {
	public :
		string s1;
		int a;
		Node(string s, int val=0) : s1(s), a(val) {
		}
		bool ok(Node other) const {
			return s1 == other.s1;
		}
};

int main() {
	Node x("xxxxx");
	cout << x.ok(string("xxxxx")) << endl;  // 隐式
	cout << x.ok(Node("xxxxx")) << endl;    // 显式
	return 0;
}

extern "C"

函数调用过程

每一个函数调用都分配一个函数栈,先将返回地址入栈,在将当前函数的栈指针入栈,然后在栈内执行函数。

C
  1. 函数返回值时,生成一个临时变量,返回该临时变量,然后在调用处把该临时变量赋值给左侧变量。
  2. 函数返回引用时,返回和接收应是 int& 类型,不能返回局部变量的引用。不生成临时变量,直接返回该引用。
代码语言:javascript
复制
#include<iostream>
using namespace std;

int x, y;

int get1() {
	cout << "get1 中 x 的地址" << &x << endl;
	return x;
}

int& get2() {
	cout << "get2 中 y 的地址" << &y << endl;
	return y;
}

int main() {
	int x = get1();
	cout << "main 中 x 的地址" << &x << endl;
	int& y = get2();
	cout << "main 中 y 的地址" << &y << endl;	
	return 0;
}
/*
get1 中 x 的地址0x4c600c
main 中 x 的地址0x6efef8
get2 中 y 的地址0x4c6010
main 中 y 的地址0x4c6010
*/

不能,会造成无限循环。

代码语言:javascript
复制
#include<iostream>
using namespace std;

class Node {
	public:
		int x;
		Node(int a) : x(a) {
		}
		Node(Node& a){	// 方法1,正确
			x = a.x;
		}
		Node(Node a) {	// 方法2,错误,无法编译通过
			x = a.x;
		}
};

int main() {
	Node x(10);
	Node y(x);
	return 0;
}

动态内存分配

代码语言:javascript
复制
int *p = (int*)malloc(sizeof(int)*100);
free(p);
int *a = new int;
delete a;
int *q = new int[100];
delete[] q;

A* a = new A; a->i = 10; 在内存分配上发生了什么?

是标准函数库

是 ++ 运算符

从堆分配内存

从自由存储区分配内存

需要显式指出分配内存大小

编译器自行计算

不会调用构造/析构函数

会调用构造/析构函数

返回无类型指针 ()

返回有类型指针

不可调用

可以基于

不可被重载

可以被重载

构造函数分为初始化和计算两个阶段,第一阶段对应初始化列表,第二阶段对应函数主体,引用必须在第一阶段完成。

代码语言:javascript
复制
#include <iostream>
using namespace std;

class Node {
public:
	int& a;
	int b, c, d;
	Node(int &x, int y, int z, int k) : a(x), b(y), c(z), d(k) {
	}
};

int main() {
	int t = 1;
	Node x(t, 2, 3, 4), y(t, 2, 3, 4);
	x.a++;
	cout << y.a << endl;
	return 0;
}

代码语言:javascript
复制
const int x = 10;
const int &y = x;
const int &z = 20;

代码语言:javascript
复制
#include<bits/stdc++.h>
using namespace std;

void func(int& x) {
	cout << "左值引用" << endl;
}
void func(int&& x) {
	cout << "右值引用" << endl;
}
void func(const int& x) {
	cout << "const 左值引用" << endl;
}
void func(const int&& x) {
	cout << "const 右值引用" << endl;
}

template<typename T> void fun(T&& x) {
	func(forward<T>(x));
}

int main() {
	fun(10);
	
	int x = 0;
	fun(x);
	fun(move(x));
	
	const int y = 0;
	fun(y);
	fun(move(y));
    return 0;
}
/*
右值引用
左值引用
右值引用
const 左值引用
const 右值引用
*/

通过红黑树实现

通过 表实现

操作复杂度 级别

操作复杂度常数级别

内部有序

内部无序

适用于对顺序有要求的场景

适用于频繁查找的场景

vector

list

类型

动态数组

动态链表

底层实现

数组实现

双向链表实现

访问

支持随机访问,

不支持随机访问,

插入

在末尾 ,在中间

很快,

删除

在末尾 ,在中间

很快,

内存来源

从堆区分配空间

从堆区分配空间

内存使用

是顺序内存

不是顺序内存

内存分配

一次性分配好,不够时扩容

每次插入节点都需要进行内存申请

性能

访问性能好,插入删除性能差

插入删除性能好,访问性能差

适用场景

经常随机访问,不在乎插入和删除效率

经常插入删除,不在乎访问效率

代码语言:javascript
复制
#include <bits/stdc++.h>
using namespace std;

int main() {
	vector<int> g;
	for(int i=1; i<=10; i++)
		g.push_back(i);
	cout << "capacity = " << g.capacity() << endl;
	vector<int>().swap(g);
	cout << "capacity = " << g.capacity() << endl;
	return 0;
}

代码语言:javascript
复制
#include <bits/stdc++.h>
using namespace std;

int main() {
	{
		set<int> st = {1, 2, 3, 4, 5, 6};
		for(set<int>::iterator iter=st.begin(); iter!=st.end(); ) {
			if(*iter == 3) {
				st.erase(iter++);   // 传给 erase 的是 iter 的一个副本
			} else {
				iter++;
			}
		}
	}
	{
		vector<int> g = {1, 2, 3, 4, 5, 6};
		for(vector<int>::iterator iter=g.begin(); iter!=g.end(); ) {
			if(*iter == 3) {
				iter = g.erase(iter);
			} else {
				iter++;
			}
		}
	}
	return 0;
}

代码语言:javascript
复制
#include <bits/stdc++.h>
using namespace std;

void print() {
	cout << endl;
}

template<class T, class... Args>
void print(T num, Args... rest) {
	cout << num << " ";
	print(rest...);
}

int main() {
	print(1, 2, 3, 4, 5);
    return 0;
}

代码语言:javascript
复制
// head.h 内容
struct Node {
	int a, b;
};

// main.h 内容
Node node

/// main.cpp 错误写法
#include "main.h"
#include "head.h"

int main() {	
	return 0;
}

// main.cpp 正确写法
#include "head.h"
#include "main.h"

int main() {	
	return 0;
}

使用尖括号和双引号的区别在于:编译器预处理阶段寻找头文件的路径顺序不一样。

  • 使用双引号的查找顺序
    1. 当前头文件目录
    2. 编译器设置的头文件路径
    3. 系统变量指定的头文件路径
  • 使用尖括号的查找顺序
    1. 编译器设置的头文件路径
    2. 系统变量指定的头文件路径

内存泄漏

内存溢出

程序申请内存的时候,超出了系统实际分配给你的空间,此时系统无法完成满足你的需求,就会发生内存溢出。

内存溢出的情况:

  • 内存中加载的数据量过于庞大。
  • 代码中存在死循环或者递归过深导致栈溢出。
  • 内存泄漏导致内存溢出。

段错误

代码语言:javascript
复制
#include<bits/stdc++.h>
using namespace std;

class A;
class B;
class A {
public:
	A() {
		cout << "A Created" << endl;
	}
	~A() {
		cout << "A Destroyed" << endl;
	}
	shared_ptr<B> ptr;
};
class B {
public:
	B() {
		cout << "B Created" << endl;
	}
	~B() {
		cout << "B Destroyed" << endl;
	}
	shared_ptr<A> ptr;
};

int main() {
	shared_ptr<A> pt1(new A());
	shared_ptr<B> pt2(new B());
	pt1->ptr = pt2;
    pt2->ptr = pt1;
    cout << "use of pt1: " << pt1.use_count() << endl;
    cout << "use of pt2: " << pt2.use_count() << endl;
    return 0;
}
/*
A Created
B Created
use of pt1: 2
use of pt2: 2
*/
代码语言:javascript
复制
#include<bits/stdc++.h>
using namespace std;

class A;
class B;
class A {
public:
	A() {
		cout << "A Created" << endl;
	}
	~A() {
		cout << "A Destroyed" << endl;
	}
	weak_ptr<B> ptr;
};
class B {
public:
	B() {
		cout << "B Created" << endl;
	}
	~B() {
		cout << "B Destroyed" << endl;
	}
	weak_ptr<A> ptr;
};

int main() {
	shared_ptr<A> pt1(new A());
	shared_ptr<B> pt2(new B());
	pt1->ptr = pt2;
    pt2->ptr = pt1;
    cout << "use of pt1: " << pt1.use_count() << endl;
    cout << "use of pt2: " << pt2.use_count() << endl;
    return 0;
}
/*
A Created
B Created
use of pt1: 1
use of pt2: 1
B Destroyed
A Destroye
*/

因为存在这种情况:申请的空间在函数结束后忘记释放,造成内存泄漏。使用智能指针可以很大程度的避免这个问题,因为智能指针是一个类,超出类的作用范围后,类会调用析构函数释放资源,所以智能指针的作用原理就是在函数结束后自动释放内存空间。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-08-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 多选参数 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 静态变量的初始化
  • 函数指针
  • 多态性和虚函数(virtual)
  • 纯虚函数
  • 静态函数和虚函数
  • 构造函数和析构函数
  • 析构函数和虚函数
  • 重载、重写(覆盖)、隐藏
  • 虚函数表
  • char* 和 char[] 的区别
  • 为什么函数参数入栈顺序从右到左
  • 宏定义和枚举的区别
  • 空类
  • 隐式类型转换
  • extern "C"
  • 函数调用过程
  • 动态内存分配
  • A* a = new A; a->i = 10; 在内存分配上发生了什么?
  • 内存泄漏
  • 内存溢出
  • 段错误
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档