不带返回值的return语句只能用于返回类型是void的函数。在返回类型为void的函数中,return返回语句不是必需的,隐式的return发生在函数的最后一个语句完成时。
一般情况下,返回类型是void的函数使用return语句是为了引起函数的强制结束,这种return的用法类似于循环结构中的break语句的作用。
示例一:交换两个整形变量数值的函数
void swap(int & a, int & b)
{
if(a == b)
{
return; //若两个值相等,无需比较,即让函数停止运行。
}
int temp;
temp = a;
a = b;
b = temp; //如果两个值不相等,则交换这两个值,隐式的return发生在最后一个赋值语句后。
}
示例二:void函数,可以返回另一个返回类型同样是void的函数的调用结果
#include
using namespace std;
void do_swap(int & a, int & b)
{
int temp = a;
a = b;
b = temp;
}
void swap_(int & a, int & b)
{
if(a == b)
{
return; //不带返回值的return语句只能用于返回类型为void的函数
}
return do_swap(a, b); //void函数,可以返回另一个返回类型同样是void的函数的调用结果
}
int main()
{
int a = 3, b = 4;
swap_(a, b);
cout << a << endl;
cout << b << endl;
return 0;
}
参考:C++中数组作为函数参数
#include
using namespace std;
int GetSize(int data[])
{
return sizeof(data);
}
int main()
{
int data1[] = {1, 2, 3, 4, 5};
int size1 = sizeof(data1);
cout << size1 << endl; //20
int data2[100];
int size2 = sizeof(data2);
cout << size2 << endl; //400
int *data3 = data1;
int size3 = sizeof(data3);
cout << size3 << endl; //8
int size4 = GetSize(data1);
cout << size4 << endl; //8
return 0;
}
data1是一个数组,sizeof(data1)是求数组的大小。这个数组包含5个整数,每个整数占4个字节,因此总共是20个字节。
data3声明为指针,对于指针求sizeof,64位机器上指针占8个字节。
C/C++中,当数组作为函数的参数进行传递时,数组自动变为同类型的指针。因此函数GetSize的参数data被声明为数组,但是它会变为指针,故size4的结果仍然为8。
不允许拷贝和赋值,不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。
int a[] = {0, 1, 2}; //含有三个整数的数组
int s2 = a; //错误,不允许使用一个数组初始化另一个数组
a2 = a; //错误,不能将一个数组直接赋值给另一个数组
使用数组是通常将将其转化成指针 ,在C++中,指针和数组有非常紧密的联系,使用数组的时候编译器会将其转化成指针。
通常情况下,使用取地址符获取指向某个对象的指针,取地址符也可以用于任何对象。数组的元素也是对象,对数组使用下标运算符得到该数组指定位置的元素。因此像其他对象一样,对数组的元素使用取地址符就能获取指向该元素的指针。
string nums[] = {"one", "two", "three"}; //数组元素是string对象
string *p = &nums[0]; //p指向nums的第一个元素
string *p2 = nums; //等价于p2 = &nums[0]
数组还有一个特性,在很多使用数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针
在大多数表达式中,使用数组类型的对象其实是使用一个指向该数组首元素的指针。
因为不能拷贝数组,所以无法以值传递的方式使用数组参数。因为数组会被转换成指针,当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式。
//三个函数等价,每个函数都有一个const int*类型的形参
void print(const int *);
void print(const int []);
void print(const int [10]); //这里的维度表示我们期望数组含有多少元素,实际不一定
和其他使用数组的代码一样,以数组作为形参的函数必须确保使用数组时不会越界。
对于高维数组而言
可以使用数组名作为实参或者形参,在被调用函数中对形参数组定义时可以指定所有维数的大小,也可以省略第一维度的大小说明
void fun(int a[3][10]);
void fun(int a[][10]);
//但是不能把第二维度或者更高维度的大小省略
void fun(int a[][]); //不合法
void fun(int a[3][]); //不合法
因为从实参传递来的是数组的起始地址,在内存中按数组排列规则存放(按行存放),而并不区分行和列。如果在形参中不说明列数,则系统无法决定应为多少行多少列,不能只指定第一维度而不指定第二维度。对于数组int p[m][n],编译器寻址,它的地址为:p + i * n + j。倘若省略第二维或者更高维度,编译器将不知道如何正确寻址。
实参数组维度可以大于形参数组:
int a[5][10];
void fun(int a[3][10]);
//此时形参数组只取实参数组的一部分,其余部分不起作用
二维数组形参传值的三种方式
//传数组,第二维度必须声明
void display1(int a[][4], const int row)
{
for(int i = 0; i < row; i++)
{
for(int j = 0; j < 4; j++)
{
cout << a[i][j] << " ";
}
cout << endl;
}
}
//一重指针,传数组指针,第二维度必须声明
void display2(int (*a)[4], const int row)
{
for(int i = 0; i < row; i++)
{
for(int j = 0; j < 4; j++)
{
cout << a[i][j] << " ";
}
cout << endl;
}
}
//使用二维指针传值
void display3(int **a, int row, int col)
{
for(int i = 0; i < row; i++)
{
for(int j = 0; j < col; j++)
{
cout << *((int*)a + col * i + j) << endl;
}
cout << endl;
}
}
一个指针占几个字节?
指针就是地址,地址就是指针。而地址是内存单元的编号。因此一个指针占几个字节,等于是一个地址的内存单元编号有多长。
在计算机中,CPU不能直接与硬盘进行数据交换,CPU只能直接跟内存进行数据交换。而CPU是通过地址总线、数据总线、控制总线三条线与内存进行数据传输与操作。
假如,我们想通过CPU在内存中读取一个数字3,如何操作?
地址总线的宽度决定了CPU的寻址能力
控制总线决定了CPU对其他控件的控制能力以及控制方式
数据总线的宽度决定了CPU单次数据传输的传送量,也就是数据传输速度
我们平时所说计算机是64位、32位、16位,指的是计算机CPU中通用寄存器一次性处理、传输、暂时存储的信息最大长度。即CPU在单位时间内能一次处理的二进制数的位数。
参考:32位CPU最多支持4G内存是怎么算出来的?
实际上内存是把8个bit排成一组,每1组成为1个单位,大小是1byte(字节),cpu每次只能访问1个byte,而不能单独去访问具体的1bit,1个byte就是内存最小的IO单位。因此“32位操作系统”中的“位”并不是内存中的“bit”的概念,对应到内存中其实是“字节byte”,所以有2^32 = 4GB的说法
地址总线为32,指针占4字节,内存最大为2^32Byte = 2^2GB = 4GB。
声明:声明是用来告诉编译器变量的名称和类型,而不分配内存。
定义:定义是为了给变量分配内存,可以为变量赋初值。
即使是extern,如果给变量赋值了,就是定义了。
全局变量或静态变量初始值为0,局部变量初始化为随机值。
在C/C++中,变量的声明和定义区别并不大,定义和声明往往是同时发生的,变量定义时,会根据变量类型分配空间。(定义也是声明;如果声明有初始化式,就被当作定义;除非有extern关键字,否则都是变量的定义)
extern int var; //声明
extern int ble = 10; //定义
typedef int INT; //声明
struct Node; //声明
int value; //定义
struct Node{
int left;
int right;
};
以下三个实体的定义也可以放到头文件中
一个由C/C++编译过的程序占用的内存分为以下几个部分
堆(heap)和栈(stack)的区别
参考:堆(heap)和栈(stack)的区别
根据变量的位置可以分为全局变量和局部变量
根据变量的静态属性可以分为静态变量和非静态变量
根据变量的const属性可以分为const变量和非const变量
针对上述的变量分类,变量的初始化分为以下几种:
全局变量和局部变量的初始化:
全局变量和局部变量的不同主要体现在变量的作用范围上,全局变量的初始化分为两种,非类对象的变量初始化发生在函数的编译阶段,如果我们没有显示的初始化,编译器会默认初始化,类对象的全局变量初始化发生在程序运行阶段的main函数之前。
对于局部变量,不会执行默认初始化,因此在使用局部变量之前必须先进行变量的初始化。
静态变量和非静态变量的初始化:
静态变量根据其位置可以分为三种:全局静态变量、定义在函数中的静态变量以及定义在类中的静态变量。
静态变量的初始化:编译器要求不同,有的要求必须主动完成初始化,有的编译器会完成默认初始化,但是值不确定。所以,在使用静态变量的时候,我们一般要求必须在定义的同时完成初始化。对于g++编译器,如果静态变量是全局或者函数的局部变量,都会完成默认初始化;但是如果类包含静态数据成员,C++要求这个静态数据成员仅可以在类内部完成声明,然后在类外,且任何函数之外,完成静态成员的初始化,初始化可以赋初始值,也可以不赋值,不赋值会默认初始化。
由于类的静态数据成员所有对象共有,所以类在分配内存的时候并不会主动分配这个静态数据成员的内存,因此要求我们必须主动要求分配内存。必须在类外完成初始化的原因是,这个静态数据成员保存在数据段(全局区),如果在函数内初始化,意味着要求编译器在栈区为这个变量分配内存,因而错误。但是如果类的静态数据成员是const,那么这个变量必须在类内完成初始化。
const对象和非const对象的初始化:
const对象必须在定义的时候进行初始化,引用的本质是一个const指针,因此引用也必须在定义的时候进行初始化。
程序示例:
int a = 0; //全局初始化区
char *p1; //全局未初始化区
int main()
{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //p3在栈上,"123456\0"在常量区
static int c = 0; //全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
//分配得来的10和20字节的区域在堆区
strcpy(p1, "123456"); //"123456\0"在常量区
return 0;
}
异常处理就是处理程序中的错误。(throw、try、catch、finally)
程序运行时常会碰到一些异常情况(对于异常情况,不能发现加以处理,会导致程序崩溃),例如
当发生异常,程序无法沿着正常的顺序执行下去的时候,立即结束程序可能并不妥当,我们需要给程序提供另外一条可以安全退出的路径。在结束前做一些必要的工作,如将内存中的数据写入文件、关闭打开的文件、释放动态分配的内存空间等。
MSVC(MSVC是微软Windows平台Visual Studio自带的C/C++编译器)
GCC原名GNU C Compiler、G++,后来逐渐支持更多的语言编译(C++、Fortran、Pascal、Objective-C、Java、Ada、Go等),是一套由GNC工程开发的支持多种编程语言的编译器。GCC是大多数类Unix(如Linux、BSD、Mac OS X等)的标准编译器,而且适用于Windows(借助其他移植项目实现的,比如MingW、Cygwin等)。GCC支持多种计算机体系芯片,如x86、ARM,并已移植到其他多种硬件平台。
Clang,是一个由Apple主导编写,基于LLVM(Low Level Virtual Machine的简称)的C/C++/Objective-C编译器,主要用于Mac OS X平台的开发。
集成开发环境
代码编辑器 + MinGW
直接使用GCC
适合于有linux使用经验且对vim比较熟悉的人,对于gcc编译,需要了解makefile的编写。
安装编译器在命令行:# apt-get install gcc g++
编写代码: # vim helloworld.cpp
编译在命令行执行 # g++ code.cpp -o exefile
参考:c++ auto基本用法
总述:
auto的原理就是根据后面的值,来自己推测前面的类型是什么。
auto的作用就是为了简化变量初始化,如果这个变量有一个很长很长的初始化类型,就可以用auto代替。
注意点:
1.用auto声明的变量必须初始化(auto是根据后面的值来推测这个变量的类型,如果后面没有值,自然会报错)
2.函数和模板参数不能被声明为auto(原因同上)
3.因为auto是一个占位符,并不是一个他自己的类型,因此不能用于类型转换或其他一些操作,如sizeof和typeid
4.定义在一个auto序列的变量必须始终推导成同一类型
auto x1 = 5, x2 = 5.0; //错误, 必须推导为同一类型
auto x1 = 5, x2 = 6; //正确
int main()
{
std::vector ve;
ve.push_back("wqsd");
std::vector::iterator it = ve.begin();
auto it_a = ve.begin(); //auto 可以用来代替长类型
cout << *it << endl;
cout << *it_a << endl;
return 0;
}
对于数组或者容器(可用迭代器的对象)情况下
参考:C++ for(auto &a:b)、for(auto a:b)、for(const auto &a:b)
b为数组或容器,是被遍历的对象
for(auto &a:b),循环体中修改a,b中对应内容也会修改
for(auto a:b),循环体中修改a,b中内容不受影响
for(const auto &a:b),a不可修改,用于只读取b中内容
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
//for(const auto &a:b) a不可修改,用于只读取b中内容
for(auto a: arr) //for(auto a:b) 循环体中修改a, b中内容不受影响
{
cout << a << ' ';
a++;
}
cout << endl;
for(int i = 0; i < 5; i++)
{
cout << arr[i] << ' ';
}
cout << endl;
for(auto &a: arr) //for(auto &a: b) 循环体中修改a, b中对应内容也会修改
{
cout << a << ' ';
a++;
}
cout << endl;
for(int i = 0; i < 5; i++)
{
cout << arr[i] << ' ';
}
cout << endl;
return 0;
}
1.
a++ 和 ++a 都属于自增运算符,区别在于对变量a的值进行自增的时机不同。a++是先进行取值,再进行自增;++a是先进性自增,再进行取值。++运算符优先级较高
2.
逗号运算符和逗号分隔符, 赋值运算符的优先级别高于逗号运算符,逗号运算符(a, b)会先执行a再执行b。
3.
struct st{
int *p;
int i;
chat a;
};
int sz = sizeof(struct st);
结构体的总大小,即为sizeof的结果,必须是其内部最大成员的“最宽基本类型成员”的整数倍,不足的要补齐。
4.C++中class和struct的区别
【C++】struct和class的区别_忽晴忽雨江湖的博客-CSDN博客_struct和class的区别
答案: 超全面的后端开发C/C++面经整理
C++中,内存分成5个区域, 堆, 栈, 自由存储区, 全局/静态存储区、常量存储区
(2)堆和栈的区别
(3)堆块还是栈快
(4)new和delete如何实现的, new和malloc的异同处
new和malloc都会分配空间,但是new还会调用对象的构造函数进行初始化, malloc需要给定空间大小, 而new只需要对象名
(5)既然有malloc和free, C++中为什么还需要new/delete呢
(6)C和C++的区别
C++中还有函数重载和引用等概念, C没有
(7)delete和delete[]的区别
(8)C++、Java的联系与区别, 包括语言特性、垃圾回收、应用场景
(9)C++和python的区别
python的基本数据类型只有 数字、布尔值、字符串、列表、元组等等
(10)struct和class区别
(11)define和const的联系区别
(12)在C++中const的用法
(13)C++中的static的用法和意义
(15)C++的STL实现
内存管理allocator、函数、实现机理、多线程实现等、
算法、容器、迭代器
(16)STL源码中的hash表的实现
unordered_map的底层实现时hashtable, 采用开链法解决哈希冲突。一定情况下, 自动转为红黑树进行组织
(17)解决哈希冲突的方式
(18)STL中的unordered_map 和 map的区别
unordered_map使用哈希实现, 占用内存多,查询速度快,内部无序
map底层采用红黑树实现, 插入删除时间复杂度O(log(n)), 内部有序
(19)STL的vector实现
(20)频繁对vector调用push_back()对性能影响和原因
(21)C++中的vector和list的区别
(22)C++中的重载和重写的区别
重载(overload)是指函数名相同,参数列表不同的函数实现方法,其返回值可以不同。
重写(overwrite)是指函数名相同, 参数列表相同,只有方法体不相同的实现方法, 用于子类继承父类时对父类方法的重写。子类的同名方法屏蔽了父类方法的现象称为隐藏。
(23)C++内存管理
在C++中,内存分成5个区,他们分别是堆、栈、全局/静态存储区和常量存储区和代码区。
(24)面向对象的三大特性
(25)多态的实现
C++ 多态包括编译时多态和运行时多态,编译时多态体现在函数重载和模板上,运行时多态体现在虚函数上。
【26 - 31】虚函数相关
(26)C++虚函数相关(虚函数表, 虚函数指针),虚函数的实现原理
(27)实现编译器处理虚函数表应该如何操作
(28)基类的析构函数一般写成虚函数的原因
如果析构函数不被声明成虚函数, 则编译器实现静态绑定,在删除指向子类的父类指针时, 只会调用父类的析构函数而不调用子类析构函数(造成内存泄漏)
(29)构造函数为什么一般不定义为虚函数
(31)纯虚函数
(32)静态绑定和动态绑定
(33)深拷贝和浅拷贝的区别(举例说明深拷贝的安全性)
深拷贝可以避免重复释放和写冲突。例如使用浅拷贝的对象进行释放后,对原对象的释放会导致内存泄漏或程序崩溃。
(34) 对象复用的了解,零拷贝的了解
(35)C++所有的构造函数
C++中的构造函数主要有三种类型:默认构造函数、重载构造函数和拷贝构造函数
(36)什么情况下会调用拷贝构造函数(三种情况)
(37)结构体内存对齐方式和为什么要进行内存对齐?
因为结构体的成员可以有不同的数据类型,所占的大小也不一样。同时,由于CPU读取数据是按块读取的,内存对齐可以使得CPU一次就可以将所需的数据读进来。
(38)内存泄露的定义,如何检测与避免?
动态分配内存所开辟的空间,在使用完毕后未手动释放,导致一直占据该内存,即为内存泄漏。
造成内存泄漏的几种原因:
1)类的构造函数和析构函数中new和delete没有配套
2)在释放对象数组时没有使用delete[],使用了delete
3)没有将基类的析构函数定义为虚函数,当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄露
4)没有正确的清楚嵌套的对象指针
避免方法:
malloc/free要配套
使用智能指针;
将基类的析构函数设为虚函数;
(39)C++的智能指针有哪些
C++中的智能指针有auto_ptr,shared_ptr,weak_ptr和unique_ptr。智能指针其实是将指针进行了封装,可以像普通指针一样进行使用,同时可以自行进行释放,避免忘记释放指针指向的内存地址造成内存泄漏。
auto_ptr是较早版本的智能指针,在进行指针拷贝和赋值的时候,新指针直接接管旧指针的资源并且将旧指针指向空,但是这种方式在需要访问旧指针的时候,就会出现问题。
unique_ptr是auto_ptr的一个改良版,不能赋值也不能拷贝,保证一个对象同一时间只有一个智能指针。
shared_ptr可以使得一个对象可以有多个智能指针,当这个对象所有的智能指针被销毁时就会自动进行回收。(内部使用计数机制进行维护)
weak_ptr是为了协助shared_ptr而出现的。它不能访问对象,只能观测shared_ptr的引用计数,防止出现死锁。
(40)调试程序的方法
(45)C11特性
自动类型推导auto:auto的自动类型推导用于从初始化表达式中推断出变量的数据类型。通过auto的自动类型推导,可以大大简化我们的编程工作
nullptr
:nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0,而nullptr是void*类型的
lambda表达式:它类似Javascript中的闭包,它可以用于创建并定义匿名的函数对象,以简化编程工作。Lambda的语法如下:
[函数对象参数](操作符重载函数参数)mutable或exception声明->返回值类型{函数体}
thread类和mutex类
新的智能指针 unique_ptr和shared_ptr
(48)string的底层实现
string继承自basic_string,其实是对char*进行了封装,封装的string包含了char*数组,容量,长度等等属性。
string可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2*n),然后将原字符串拷贝过去,并加上新增的内容。
(51)定义和声明的区别
声明是告诉编译器变量的类型和名字,不会为变量分配空间
定义就是对这个变量和函数进行内存分配和初始化。需要分配空间,同一个变量可以被声明多次,但是只能被定义一次
(52)typdef和define区别
#define是预处理命令,在预处理是执行简单的替换,不做正确性的检查
typedef是在编译时处理的,它是在自己的作用域内给已经存在的类型一个别名
(53)被free回收的内存是立即返还给操作系统吗?为什么
https://blog.csdn.net/YMY_mine/article/details/81180168
不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。
(58)为什么C++没有实现垃圾回收?
C/C++ 数组作为参数传递 指针 引用