1. 程序的执行过程
- 代码区与常量区(只读区):真正的常量存储在这里,比如“abc”,"88"等字符串和数字。而const关键字只是让编译器把变量视为常量罢了,和真正的常量有本质区别。
- 栈区:函数执行所需的空间,当函数执行完毕,则对应的占内存将全部销毁。
- 堆区:进程用来灵活分配内存的地方,需要栈区指针来指向。
- 静态变量区(可读写区):用来存储静态变量和全局变量的地方。
2. new关键字
new关键字是C++用来动态分配内存的主要方式。
/*
* 动态分配对象
* 对于类,会使用对应构造函数,没有对应构造函数会报错
**/
int* pI = new int; // 不初始化,pI是未定义的,不知道指向哪里
int* pI = new int(100); // 栈中保存pI指针,指向堆区的内存,栈可以调用pI来操作堆区的对应内存
delete(pI); // 使用了new的对象,一定要delete
/*
* 动态分配数组
* 对于类数组,有没有()都一样,均使用默认构造函数,如果没有默认函数会报错
**/
int* pI = new int[50]; // 没有()即不初始化,pI是未定义的,不知道指向哪里
int* pI = new int[50](); // 使用[]初始化数组,有()则全初始化为0
delete[] pI; // 释放数组,需要加[]
std::string* pString = new std::string[50]("111"); // 错误,只能使用默认构造函数
内存泄漏会出现很严重的问题!!!
3. 命名空间
团队合作时会常常出现起名重复的问题,命名空间就是来解决这个问题的。
/*
* A文件
**/
#include<iostream>
void test()
{
std::cout << "A_test()" << std::endl;
}
/*
* B文件
**/
#include<iostream>
void test()
{
std::cout << "B_test()" << std::endl;
}
/*
* main()函数
**/
#include"ATest.h"
#include"BTest.h"
int main()
{
test(); // 此时系统不知道调用哪个test()
}
使用命名空间很容易就解决上述问题。
/*
* A文件
**/
#include<iostream>
namespace A // 命名空间A
{
void test()
{
std::cout << "A_test()" << std::endl;
}
}
/*
* B文件
**/
#include<iostream>
namespace B // 命名空间B
{
void test()
{
std::cout << "B_test()" << std::endl;
}
}
/*
* main()函数
**/
#include"ATest.h"
#include"BTest.h"
using A::test; // 法一:使用using明确需要的函数
using namespace std; // 法二:使用using namespace明确需要的命名空间
int main()
{
A::test(); // 法三:调用A命名空间的函数
std::cout; // 所有C没有的函数,而C++有的都定义在std的命名空间中
}
注意:头文件中一定不要使用using关键字,不然会导致命名空间的污染。
4. const关键字
规则:const默认与左边结合,左边没有东西则与右边结合。
/*
* 常量数值:指向的值不能更改
* 常量指针:指针的指向不能更改
**/
const int* p; // const与int结合,即 p为指向常量数值的指针
int const* p; // const与int结合,即 p为指向常量数值的指针
int* const p; // const与*结合,即 p为指向变量数值的常量指针
const int* const p; // 第一个const与int结合,第二个与*结合,即 p为指向常量数值的常量指针
5. auto关键字
auto主要用于类型转换
int i = 100;
auto i2 = i; // auto自动推断出int,即i2为int类型
- auto只能推断出类型,引用不是类型,所以auto无法推断出引用,要使用引用只能自己加引用符号。
int i = 100;
auto& i2 = i; // i2类型为int&
- auto关键字在推断引用类型时,会直接将引用类型替换为引用指向的对象。其实引用就是个别名,我们在复制时,不管中间什么类型,都是一样的。
int i = 100;
const int& refI = i;
auto i2 = refI; // 相当于 auto i2=i
- auto关键字在推断类型时,会忽略值类型的const修饰,而保留修饰指向对象的const,即会保留指针,因为修改auto的变量不会对原来的值造成影响,因此不用保留。
/*整型*/
const int i = 100; // const修饰int,值类型的修饰被忽略
auto i2 = i; // i2的类型为int
/*指针*/
int i = 100;
const int* const pI = &i; // 第二个const修饰指针即为值类型的const,第一个const保留
auto pI2 = pI; // pI2的类型为const int*
- auto关键字在推断类型时,如果有了引用符号,那么值类型的const和修饰指向对象的const都会保留,引用必须保留const,不然原来的常量也可以修改了。
/*整型*/
const int i = 100;
auto& i2 = i; // i2的类型为const int&
/*指针*/
int i = 100;
const int* const pI = &i;
auto& pI2 = pI; // pI2的类型为const int* const&
- auto可以在前面加上const。
int i = 100;
const auto i2 = i; // i2类型为const int
- auto不会影响编译速度,甚至会加快编译速度。
XX A = B // 当XX为传统类型时,编译器会检查B的类型是否可以转换为XX
auto A = B // 编译器直接按照B的类型给A
- 如果不明确转换的类型,不要滥用auto。
- auto主要用于在模板相关的代码中。
6. 静态变量区
变量的存储位置:静态变量区、堆区、栈区
静态变量区在编译时就已经确定地址,存储全局变量和静态变量。
unsigned g_i = 0; // 全局变量,在编译的时候就初始化了
unsigned test()
{
static unsigned callCount = 0; // 静态变量,在编译时就初始化了,运行时不执行这行代码
return ++callCount;
}
int testStatic()
{
test();
test();
test();
unsigned testFuncCallCount = test();
return 0;
}
7. 指针与引用
指针都是存储在栈上或者堆上的,不管在栈上还是堆上,都一定有一个地址。
&a:a这个变量的地址
a:a这个变量的地址所存储的值
*a:以a的值为地址的地方所存储的值
指针能灵活操作内存,所以引用被发明了,引用就是阉割版的指针,即类型&===类型 * const。所以引用必须一开始就赋初值,因为常量必须赋初值,即引用相当于常量。
int i = 20;
int* p = &i; // 在内存上,i与p相邻存储,i存放20,p存放i的地址
/*下面两行代码一样*/
int& refI = i;
int* const pI = &i;
// *pI与refI本质是一样的,都是指向i且不可改变
const int i; // 错误,常量必须赋初值
int& refI; // 错误,引用必须赋初值
refI = i;
8. 左值与右值
8.1 左右值的分类
左值:拥有地址属性的对象叫左值。左值可以放在=左边也可以放在右边。
int i = 10;
int i2 = i; // i为左值但可以放在右边
右值:不是左值的对象就是右值,没有地址属性的值就是右值。如临时对象都是右值。
int i = 10;
(i + 1) = 11; // (i + 1)是临时对象,它有地址,但没有我们可操作的地址属性
/*
* ++i的本质:++i为左值
**/
i = i + 1;
return i; // 返回的值就是i的地址存储的值
/*
* i++的本质:i++为右值
**/
temp = i; // 创建临时变量,对temp进行操作
temp += 1;
return temp; // 返回临时变量的值
判断左值右值:直接看代码的下一行,如果可以取到上一行的地址的对象,为左值,取不到为右值。
8.2 引用的分类
- 普通左值引用,就是一个对象的别名,只能绑定左值,无法绑定常量对象。
const int i = 10;
int& refI = i; // 错误,refI可以修改,那i的const无意义
- const左值引用,可以绑定常量对象,可以绑定任何左值和右值。
const int i = 10;
const int& refI = (i + 1); // const引用绑定右值
- 右值引用,右值引用只能绑定右值。
int i = 100;
int&& rrefI = (i + 1);
int&& rrefI = i; // 错误,右值引用无法绑定左值
- 万能引用
9. 右值引用的使用
9.1 move函数
- move函数可以对一个左值使用,使操作系统不考虑其地址属性,使其完全视为一个右值。
int i = 10;
int&& rrefI = i; // 错误
int&& rrefI = std::move(i); // 正确,使用move函数,可视作右值
- move函数让操作的对象失去了地址属性,所以我们之后不能在使用操作了move后的变量的地址属性,也就是以后都不能使用该变量了。
9.2 临时对象
临时对象都是右值,而右值引用主要负责处理临时对象,程序执行时生成的中间对象就是临时对象,临时对象产生后很快就会被销毁。
10. 可调用对象
如果一个对象可以使用运算符 “()” ,()里面可以放参数,那么这个对象就是可调用对象。
10.1 函数
#include<iostream>
void test(int i)
{
std::cout << i << std::endl;
}
using pf_type = void(*) (int); // 定义函数test()的函数指针,函数指针与普通指针一样,只是多了个(参数)
void myFunc(pf_type pf, int i)
{
pf(i);
}
void mian()
{
myFunc(test, 200); // 可调用对象,myFunc为对象,()为运算符
}
10.2 仿函数
具有operator()的函数的类对象,此时类对象可以当做函数使用,称为仿函数。
10.3 lambda表达式
就是匿名函数,普通的函数在使用前需要将这个函数定义,因此提供了lambda表达式,省去了定义函数的过程。
lambda表达式的格式:最少是“[] {}”,完整的格式是“[] () -> ret{}”
void mian()
{
[] {
std::cout << "Hello" << std::endl;
}();
}
lambda各个组件介绍:
- [] 代表捕获列表,表示lambda表达式可以访问前文的哪些变量。
(1)[] 表示不捕获任何变量。
(2)[=] 表示按值捕获所有变量。
(3)[&] 表示按引用捕获所有变量。值和引用的区别与函数传递一致。
(4)[=, &i] 表示i用引用传递,其它所有变量均用值传递。
(5)[=, i] 与(4)的作用一致,表示i用引用传递,其它所有变量均用值传递。
(6)[i] 表示用值的形式单独的捕获i。
(7)[&i] 表示用引用的形式单独的捕获i。 - () 代表lambda表达式的参数,与函数类似。
[=] (int i) // 函数的参数
{
std::cout << "Hello" << std::endl;
}(10); // 函数的传值
- ->ret 表示指定lambda的返回值,如果不指定,lambda表达式也会推断出一个返回值。
auto ret = [=] (int i) -> int // -> 规定返回类型,默认为void
{
std::cout << "Hello" << std::endl;
return i; // return返回值
}(10);
- {} 代表函数体,与普通函数完全一致。
#include<iostream>
using pf_type = void(*) (int); // 定义函数test()的函数指针类型
void myFunc(pf_type pf, int i)
{
pf(i);
}
void mian()
{
myFunc([](int i) { // lambda表达式作为函数指针参数传递,其[]必须为空
std::cout << "Hello" << std::endl;
}, 30);
}
#include<iostream>
#include<functional>
using func_typr = std::function<void(int)>;
void myFunc(func_typr func, int i)
{
func(i);
}
void mian()
{
int i2 = 50;
myFunc([i2](int i) { // 使用std::fuction可以解决作为函数指针[]必须为空的问题
std::cout << "Hello" << std::endl;
} , 30);
}