C++ 万字长文第一篇---拿下字节面试
关键字的作用
静态变量的初始化
int main() {
int initNum = 3;
for (int i=1; i<=5; i++) {
static int n1 = initNum;
n1++;
printf("%dn", 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("%dn", n1);
}
return 0;
}
/*
4
4
4
4
4
*/
函数指针
在编译过程中,每一个函数都有一个入口地址,而函数指针就是指向该入口地址的指针。
#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)
- 静态多态主要表现为重载,在编译时就确定了。
- 动态多态的基础是虚函数机制,虚函数的作用是实现动态多态,在运行期间动态绑定,决定了派生类调用哪个函数。
#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()
*/
纯虚函数
有些情况下,基类生成的对象是不合理的,比如动物可以派生出狮子、孔雀等,这些派生类显然存在着较大的差异。那么可以让基类定义一个函数,并不给出具体的操作内容,让派生类在继承的时候在给出具体的操作,这样的函数被称为纯虚函数。含有纯虚函数的类成为抽象类,抽象类不能声明对象,只能用于其他类的继承。 纯虚函数的定义方法为:
void ReturnType Function() = 0;
子类可以不重写虚函数,但一定要重写纯虚函数。
静态函数和虚函数
静态函数在编译时就确定了调用它的时机,而虚函数在运行时动态绑定,虚函数由于用到了虚函数表和虚函数虚函数指针,会增加内存使用。
构造函数和析构函数
- 构造函数在每次创建对象的时候调用,函数名称和类名相同,无返回类型,构造函数可以为类初始化某些成员。
- 析构函数在每次删除对象的时候调用,函数名称和类名相同,但在前面加了一个
符号,同样无返回类型。若对象在调用过程中用
动态分配了内存,可以在析构函数中写
语句统一释放内存。
- 如果用户没有写析构函数,编译系统会自动生成默认析构函数。
- 假设存在继承:孙类继承父类,父类继承爷类
- 孙类构造过程:爷类 -> 父类 -> 孙类
- 孙类析构过程:孙类 -> 父类 -> 爷类
析构函数和虚函数
- 可能作为继承父类的析构函数需要设置成虚函数,这样可以保证当一个基类指针指向其子类对象并释放基类指针的时候,可以及时释放掉子类的空间。
- 虚函数需要额外的虚函数表和虚函数指针,占用额外的内存,所以不会作为继承父类的析构函数不用设置成虚函数,否则会浪费内存。
++默认的析构函数不是虚函数,只要当其作为父类的时候,才会设置为虚函数。
重载、重写(覆盖)、隐藏
可以通过 attribute 关键字,声明 constructor 和 destructor 来实现。
#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[] 的区别
char *s1 = "abc";
char s2[] = "abc"
int x = 10;
const int *a = &x;
int* const b = &x;
为什么函数参数入栈顺序从右到左
为了支持 不定长参数函数
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;
}
enum color{red, green, blue, yellow};
enum color2{red, green}; // ERROR,因为 red 和 green 已经在 color 中定义过了
auto x = red; //OK,因为 red 没有限定作用域
auto y = color::red; //OK
enum struct color{red, green, blue, yellow};
enum struct color2{red, green}; // OK,red 和 green 在不同作用域内
auto x = red; // ERROR,red 没有指定作用域
auto y = color::red;
强类型转换的优点在于
- 限定作用域的枚举类型将名字空间污染降低
- 限定作用域的枚举类型是强类型的,无法通过隐式转换到其他类型,而不限定的枚举类型可以自动转换为整形
宏定义和枚举的区别
- 枚举是一种实体,占内存。宏定义是一种表达式,不占内存。
- 枚举在编译阶段进行处理,宏定义在与编译阶段就完成了文本替换。
空类
隐式类型转换
- 表达式中,低精度类型向高精度类型发生转换。
- 条件语句中,非布尔类型向布尔类型发生转换。
- 初始化语句中,初始值向变量类型发生转换。
- 赋值语句中,右侧运算对象向左侧运算对象发生转换。
- 可以用 单个形参 来调用的构造函数定义了从 形参类型 到 该类类型 的一个隐式转换。注意 单个形参 并不是只有一个形参,可以有多个形参,但其他形参要有默认实参。
#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"
函数调用过程
每一个函数调用都分配一个函数栈,先将返回地址入栈,在将当前函数的栈指针入栈,然后在栈内执行函数。
- 函数返回值时,生成一个临时变量,返回该临时变量,然后在调用处把该临时变量赋值给左侧变量。
- 函数返回引用时,返回和接收应是 int& 类型,不能返回局部变量的引用。不生成临时变量,直接返回该引用。
#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
*/
不能,会造成无限循环。
#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;
}
动态内存分配
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; 在内存分配上发生了什么?
是标准函数库 |
是 ++ 运算符 |
从堆分配内存 |
从自由存储区分配内存 |
需要显式指出分配内存大小 |
编译器自行计算 |
不会调用构造/析构函数 |
会调用构造/析构函数 |
返回无类型指针 () |
返回有类型指针 |
不可调用 |
可以基于 |
不可被重载 |
可以被重载 |
构造函数分为初始化和计算两个阶段,第一阶段对应初始化列表,第二阶段对应函数主体,引用必须在第一阶段完成。
#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;
}
const int x = 10;
const int &y = x;
const int &z = 20;
#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 |
|
---|---|---|
类型 |
动态数组 |
动态链表 |
底层实现 |
数组实现 |
双向链表实现 |
访问 |
支持随机访问, |
不支持随机访问, |
插入 |
在末尾 ,在中间 |
很快, |
删除 |
在末尾 ,在中间 |
很快, |
内存来源 |
从堆区分配空间 |
从堆区分配空间 |
内存使用 |
是顺序内存 |
不是顺序内存 |
内存分配 |
一次性分配好,不够时扩容 |
每次插入节点都需要进行内存申请 |
性能 |
访问性能好,插入删除性能差 |
插入删除性能好,访问性能差 |
适用场景 |
经常随机访问,不在乎插入和删除效率 |
经常插入删除,不在乎访问效率 |
#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;
}
#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;
}
#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;
}
// 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;
}
使用尖括号和双引号的区别在于:编译器预处理阶段寻找头文件的路径顺序不一样。
- 使用双引号的查找顺序
- 当前头文件目录
- 编译器设置的头文件路径
- 系统变量指定的头文件路径
- 使用尖括号的查找顺序
- 编译器设置的头文件路径
- 系统变量指定的头文件路径
内存泄漏
内存溢出
程序申请内存的时候,超出了系统实际分配给你的空间,此时系统无法完成满足你的需求,就会发生内存溢出。
内存溢出的情况:
- 内存中加载的数据量过于庞大。
- 代码中存在死循环或者递归过深导致栈溢出。
- 内存泄漏导致内存溢出。
段错误
#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
*/
#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
*/
因为存在这种情况:申请的空间在函数结束后忘记释放,造成内存泄漏。使用智能指针可以很大程度的避免这个问题,因为智能指针是一个类,超出类的作用范围后,类会调用析构函数释放资源,所以智能指针的作用原理就是在函数结束后自动释放内存空间。
- Android调用系统相册和拍照的Demo
- 黑客是如何通过RDP远程桌面服务进行攻击的
- SDL的几个宽高概念讲解(文中有福利)
- [安全科普]你必须了解的session的本质
- Android中如何动态的实现设置全屏和退出全屏
- Android 双进程Service常驻后台,无惧“一键清理”
- Android之捕获TextView超链接
- 自封装Android软键盘工具类ImeUtil
- XSS挑战第一期Writeup
- 安全公司新星Aorato推出“行为防火墙”
- 倍数提高工作效率的 Android Studio 奇技
- xss如何加载远程js的一些tips
- Android中如何实现图文混排
- Jenkins 创始人:持续交付的 What、Why 及 How
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法