auto关键字
c++中的auto关键字是根据后面的值,来自己推测前面的类型是什么,初次见是再可用迭代器的对象中。
DAY 1
9======
C++程序执行过程中,将内存划分为以下几个区域:分别是
- 代码区(.txt段):存放函数的二进制代码,由操作系统进行管理。
- 全局区/静态存储区(.bss段和.data段):存放全局变量、静态变量
- 常量存储区(.data段):存储的是常量,不允许修改。
- 栈区:由编译器自动分配存放,存放函数的参数值、局部变量等。
- 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
从低地址到高地址排序依次是:
.txt段→.data段→.bss段→堆→unused→栈→env。
内存四区意义:
不同区域存放不同的数据,赋予不同的生命周期,灵活编程。
1.1 程序运行前
在程序编译后,生成了exe可执行程序,未执行程序前分为两个区域
代码区:
存放CPU执行的机器指令
代码区是共享的
代码区是只读的。
全局区:
全局变量和静态变量存放在此,全局区还包括常量区,字符串常量和其他常量(const修饰的变量)也存放于此。
该区域的数据在程序结束后由操作系统释放。
1.2 程序运行后
栈区:
存放由函数的参数值,局部变量等,栈区由编译器自动分配释放。
注意:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放。
堆区:
由程序员分配释放,程序员不释放时,由操作系统回收。
在c++中主要利用new在堆区开辟内存。利用delete释放内存
#if 1
#include<iostream>
using namespace std;
// 堆区创建一个变量
int* test1()
{
int* p = new int(10); //new返回的是变量的地址,因此需要指针接收
return p;
}
// 在堆区用new开辟数组
void test0()
{
int *arr=new int[10]; //数组有10个元素
for (int i = 0; i < 10; i++)
{
arr[i] = 100 + i;
}
for (int i = 0; i < 10; i++)
{
cout << arr[i] << endl;
}
//释放堆区数组的时候需要加[]
delete[] arr;
}
int main()
{
int* p = test1();
cout << *p << endl;
//堆区的释放用delete
delete p;
//cout << *p << endl;
test0();
return 0;
}
#endif
#if 1
#include<iostream>
using namespace std;
// 堆区创建一个变量
int* test1()
{
int* p = new int(10); //new返回的是变量的地址,因此需要指针接收
return p;
}
// 在堆区用new开辟数组
int * test0()
{
int *arr=new int[10]; //数组有10个元素
for (int i = 0; i < 10; i++)
{
arr[i] = i + 100;
}
return arr;
}
int main()
{
int* p = test1();
cout << *p << endl;
//堆区的释放用delete
delete p;
//cout << *p << endl;
int *p1=test0();
for (int i = 0; i < 10; i++)
{
cout << p1[i] << endl;
}
delete[] p;
for (int i = 0; i < 10; i++)
{
cout << p1[i] << endl;
}
return 0;
}
#endif
变量的区别
C++变量根据定义的位置具有不同的生命周期、具有不同的作用域,作用域可以分为6种:局部作用域、全局作用域、语句作用域、类作用域、命名空间作用域和文件作用域。
从作用域来看:
- 全局变量:全局变量具有全局作用域。全局变量只需要在一个源文件中定义,就可以作用于全部的源文件,但是要想在不包含全局变量定义的源文件中使用全局变量,需要使用exetern关键字。
- 静态全局变量:静态全局变量具有文件作用域。与全局变量不同的是,如果程序包括多个文件,静态全局变量只能作用在定义它的文件中,不能作用于其他文件。即被static修饰的变量都具有文件作用域。
- 局部变量:局部变量具有局部作用域。局部变量只在函数执行期间存在,函数执行完毕后,变量即被销毁,所占内存也被清空。
- 静态局部变量:静态局部变量具有局部作用域。与局部变量不同的是,程序运行期间一直存在,只初始化一次。与静态全局变量不同的是,静态局部变量只能作用于定义它的函数中。
从分配内存看:
静态全局变量、静态局部变量和全局变量存在于全局区(.bss)和静态存储区(.data),局部变量存在栈区。
栈和堆的区别
- 申请方式:栈是系统分配,堆是程序员主动申请
- 申请后系统响应:分配栈空间时,如果剩余空间大于申请空间,则申请成功,如果剩余空间小于申请空间,则分配失败栈溢出;堆在内存中的分布类似于链表,当系统收到申请时,会寻找链表中第一个大于申请内存的空间,将该节点从链表中删除,并且在该块内存的首地址记录下本次分配大小,这样释放的时候不会误操作,空闲的部分会重新放到内存中。
- 栈在内存中是一块连续的空间,最大容量是系统设定好的,堆在内存中的空间是不连续的
- 申请效率:栈是系统分配,效率高,但是程序员无法控制;堆由程序员主动申请,效率低,使用方便,但是容易产生碎片。
DAY2
引用:
作用:引用相当于给变量起别名
语法:(数据类型)&别名=原名 (指向同一块内存空间)
注意事项:(1)引用必须初始化;(2)引用一旦初始化就不可以更改。
引用的本质:指针常量
#include<iostream>
using namespace std;
int main()
{
int rats = 10;
int& radents = rats;//引用 ==int * const radents=&a
cout<<"rats的地址是" << int(&rats)<<endl;
cout << "radents的地址是" << int(&radents) << endl;
}
引用做函数参数:
作用:函数传参时,使函数中的变量名称为调用函数章的变量别名,这种方式称为按引用传递,按引用传递的时候,被调用函数的能够访问调用函数中的变量。
引用传递与值传递的不同:引用传递可以修改实参的值,但是值传递不行。
引用传递与地址传递的不同:引用传递简化了指针修改实参
#include<iostream>
using namespace std;
//值传递
void swap1(int a, int b)
{
int temp = a;
a = b;
b = temp;
//cout << "a=" << a << "," << "b=" << b << endl;
}
// 引用传递
void swapr(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int a = 10;
int b = 8;
swap1(a, b);// 调用值传递
cout <<"调用值传递后:" << "a=" << a << "," << "b=" << b << endl;
swapr(a, b);
cout << "调用引用传递后:" << "a=" << a << "," << "b=" << b << endl;
}
引用做函数返回值:
- 不能返回局部变量的引用,因为局部变量存放在栈区,编译器自主释放,函数执行结束后,局部变量的内存空间会释放,会导致引用失败。
- 函数的调用可以作为左值
int& test01()
{
static int a = 10; //声明成静态变量,避免内存释放的问题
return a;
}
int main()
{
int &b = test01();
cout << "b=" << b<<endl;
test01() = 1000; // 如果函数的返回值是引用,函数就可以作为左值
cout << "b=" << b<<endl;
}
常量引用:
作用:让函数传递信息,而不对信息做修改,同时向使用引用,应该使用常量引用。
方式:在函数头和函数原型中加const。
引用和指针的区别:
经常会被问到引用和指针的区别,在这里总结一下:
- 引用是给变量取别名,实际上是和变量一样东西,但是指针存储的是地址。
- 引用必须初始化,而且初始化后不可变动,但是指针不需要,指针也可以指向别的地址。
- 引用不可以为NULL,但是指针可以。
- 可以有多级指针,但是没有多级引用。
- 自增的结果不一样。
DAY 3
指针
指针的基本概念:
指针的作用:可以通过指针间接访问内存
指针变量的定义和使用:
语法:数据类 * 变量名
#include<iostream>
using namespace std;
//指针可以保存地址
int main()
{
//1、定义一个指针
//语法:数据类型 * 变量名称
int* p; //定义一个指针
int a = 10;
//指针变量赋值
p = &a;//p指向a的地址
cout << "a的地址为:" << &a << endl;
cout << "指针p为:" << p << endl;
//2、使用指针
//可以通过解引用的方式来找到指针指向的内存中的数据
//指针前加* 代表解引用
*p = 1000;
cout << "a:" << a << endl;
cout << "*p:" << *p << endl;
}
指针所占内存空间:
32位操作系统下,指针(不管任何数据类型)占用4个字节空间,64位操作系统下,占8个字节。
空指针和野指针
空指针和野指针都不是我们申请的内存空间,所以不能访问。
#include<iostream>
using namespace std;
int main()
{
//空指针
// 1、空指针用于给指针变量进行初始化
//int* p = NULL;
//2、空指针是不可以访问的
//内存中0-255是系统占用的,不允许访问
//*p = 0;
//野指针:越界的指针
// 在程序中,尽量避免野指针
int* p = (int*)0x1100;// 指针变量p指向内存地址编号为0x1100的空间
cout << *p << endl;
//
}
const修饰指针
- 常量指针
语法:const int *p=&a;
特点:指针的指向可以修改,但是指针不能通过解引用修改所指向的内存空间中的数据。
#include<iostream>
using namespace std;
int main()
{
//1、常量指针
// 语法:const int *p=&a
// 特点:指针指向的是个常量,不可以解引用进行修改,但是指针的指向可以修改
int a = 10;
int b = 20;
const int* p = &a;
//*p = 100;//报错信息:p不能给常量赋值
//cout << "*p的值为:" << *p << endl;
p = &b;
cout << "*p的值为:" << *p << endl;
}
- 指针常量
语法:int * const p=&a;
特点:指针指向不可以修改,指针指向的值可以修改
#include<iostream>
using namespace std;
int main()
{
//2、指针常量
// 语法:int * const p=&a;
// 特点:指针本身是个常量,指针的指向不可以修改,但是可以通过解引用修改存储在内存中的数据
int c = 50, d = 70;
int * const p1 = &c;
//p1 = &d; //不可以修改
*p1 = 200;
cout << "*p1的值为:" << *p1 << endl;
}
const既修饰指针又修饰常量
语法: int const *const p
特点:指向和指向的值都不能修改
#include<iostream>
using namespace std;
int main()
{
//3、既修饰指针,又修饰常量
//语法:const int * const p=&a;
//特点:指向和指向的值都不可以修改
const int* const p2 = &d;
//p2 = &c;
// *p2 = 100;
return 0;
}
指针和数组
作用:利用指针访问数组中的元素
重点:利用p++的操作实现指针偏移
#include<iostream>
using namespace std;
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//指针指向数组的首地址
int *P = arr;//数组名就是数组的首地址
cout << "第一个元素为:" << arr[0] << endl;
cout << "指针指向的元素是:" << *P << endl;
//利用指针遍历一维数组
for (int i = 0; i < 10; i++)
{
cout << "指针指向的元素是:" << *P << endl;
P++; //指针后移,指向当前指针的下一个内存空间
}
//利用指针遍历二维数组
int arr1[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
for (int i = 0; i < 3; i++)
{
int* p = arr1[i];//指向当前行的首地址
for (int j = 0; j < 3; j++)
{
cout << "指针指向的元素是:" << *p++<< endl;
}
}
}
指针和函数
重点:在C++中,数组名==数组首地址
#include<iostream>
using namespace std;
void Bubble(int* p, int n)
{
for (int i = 0; i < n-1; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
if (p[j] > p[j + 1])
{
int temp = p[j];
p[j] = p[j + 1];
p[j + 1] = temp;
}
}
}
}
void printarr(int* arr1, int n)
{
for (int i = 0; i < n; i++)
{
cout << *arr1++ << " ";
}
}
int main()
{
//1、创建数组
int arr[10] = { 2,6,8,3,5,8,10,23,67,92 };
//2、冒泡排序
int n = sizeof(arr) / sizeof(arr[0]);
Bubble(arr, n);
printarr(arr, n);
return 0;
//
}
#include<iostream>
using namespace std;
void Bubble(int* p, int n)
{
for (int i = 0; i < n-1; i++)
{
int* p1 = p;
for (int j = 0; j < n - i - 1; j++)
{
if (*(p1)>*(p1+1))
{
int temp = *p1;
*(p1) = *(p1+1);
*(p1+1) =temp;
}
p1++;
}
}
}
void printarr(int* arr1, int n)
{
for (int i = 0; i < n; i++)
{
cout << *arr1++ << " ";
}
}
int main()
{
//1、创建数组
int arr[10] = { 2,6,8,3,5,8,10,23,67,92 };
//2、冒泡排序
int n = sizeof(arr) / sizeof(arr[0]);
Bubble(arr, n);
printarr(arr, n);
return 0;
//
}
函数与二维数组
当用二维数组做函数参数时,如何正确的声明指针?
示例:如果用SUM函数对二维数组进行求和?
#include<iostream>
using namespace std;
int SUM(int(*arr)[4], int size)
{
int total = 0;
for (int i = 0; i < size; i++)
{
int* p = arr[i];
for (int j = 0; j < 4; j++)
{
total += *p++;
}
}
return total;
}
int main()
{
//int data[3][4] = { {1,2,3,4},{9,8,7,6},{2,4,6,8} };
int data[3][4] = { {1,1,1,1},{1,1,1,1},{1,1,1,1} };
/*
data是一个数组名,数组有三个元素,第一个元素本身是一个数组,由四个int值组成,
因此data的类型是一个指向四个int类型组成的数组的指针,
因此函数可以有以下几种定义方式
int SUM(int arr[][4],int size)
int SUM(int *arr[4},int size)
*/
int row = sizeof(data)/sizeof(data[0]);
int sum_value = SUM(data, row);
cout << sum_value << endl;
}
DAY4
c++ lower_bound()函数
在C++中的查找函数中,有find()、find_if()、search()等。但是这些函数的底层都是采用的逐个遍历的方式,执行效率并不高,当指定区域中的数据区域有序状态时,如果想查找某个目标元素,往往采用二分查找。lower_bound()就是c++ STL提供的查找函数,其底层实现就是二分查找。
作用
lowe_bound()函数用于查找指定区域内第一个不小于目标值的元素,
语法
//在 [first, last) 区域内查找不小于 val 的元素 ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val); //在 [first, last) 区域内查找第一个不符合 comp 规则的元素 ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val, Compare comp);
代码
#include<bits/stdc++.h>
using namespace std;
class mycmp {
//运算符重载
public:
//重载()运算符
bool operator()(const int& a, const int& b)
{
return a >b;
}
};
int main()
{
int a[5] = { 1,2,3,4,5 };
//找到第一个不小于3的数
int* p = lower_bound(a, a + 5, 3);
cout << *p << endl;
vector<int>myvector = { 4,5,3,2,1 };
//遇到一个不比3大的数就会返回地址指针
vector<int>::iterator iter = lower_bound(myvector.begin(), myvector.end(), 3, mycmp());
cout << *iter << endl;
}
C++ upper_bound()函数
作用
upper_bound()函数用于查找指定范围内大于目标值的第一个元素
语法
//查找[first, last)区域中第一个大于 val 的元素。 ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last, const T& val); //查找[first, last)区域中第一个不符合 comp 规则的元素 ForwardIterator upper_bound (ForwardIterator first, ForwardIterator last, const T& val, Compare comp);
代码
#include<bits/stdc++.h>
using namespace std;
class mycmp {
public:
bool operator()(const int& a, const int& b)
{
return a > b;
}
};
int main()
{
int a[5] = { 1,2,3,4,5 };
//找到第一个大于3的元素
int* p = upper_bound(a, a + 5, 3);
cout << *p << endl;
vector<int>myvector{ 4,5,3,1,2 };//这里注意一下 容器是怎么初始化的
vector<int>::iterator iter = upper_bound(myvector.begin(), myvector.end(), 3, mycmp());
cout << *iter << endl;
}
lower_bound()和upper_bound()的区别
lower_bound()找到第一个不小于目标元素的值,可以是大于以可以是等于,upper_bound()是找到第一个大于目标值的元素。
sort函数
在做题目的时候,需要对vector按照右值进行升序排序,因此自己写了一个cmp函数,
sort(intervals.begin(),intervals.end(),[](const vector<int> a,const vector<int> b){return a[1]<b[1];});
但是在运行的时候,超时了,但是换成下面的方式就可以
sort(intervals.begin(),intervals.end(),[](const auto&a,const auto& b){return a[1]<b[1];});
思考了一下为什么? 如果是第一种函数,重载了[ ],但是运用的是值拷贝函数,如果换成第二种函数的话,运用值传递方式,可以节省时间。
DAY5
左值引用和右值引用
什么是左值和右值
左值是在内存中有确定的内存地址和变量名,表达式结束后仍然存在的值
右值的话在内存中没有地址,在表达式结束后就不存在了。
左值引用
在DAY2的时候讲过什么是引用,对左值进行引用就是左值引用,左值引用包括常量左值引用和变量左值引用。
#include<bits/stdc++.h>
using namespace std;
int main()
{
//非常量左值
int a = 10;//a是非常量左值,有确定的内存地址,有变量名
//常量左值
const int b = 20; //b是常量左值,有确定的内存地址,有变量名
const int c = 30; //c是常量左值,有确定的内存地址,有变量名
//非常量引用
int& a_ref = a; //非常量左值引用a可以被非常量左值引用a_ref绑定
int& b_ref = b; //常量左值b不可以被非常量左值引用b_ref绑定
int& bc_ref =(c + b); //(c+b)是常量右值,不可以被非常量左值引用bc_ref绑定
//常量引用
const int& c_a_ref = a; //非常量左值引用a可以被常量左值引用c_a_ref绑定
const int& c_b_ref = b;//常量左值b可以被常量左值引用c_b_ref绑定
const int& c_bc_ref = (b + c); //常量右值(b+c)可以被常量左值c_bc_ref绑定
const int& c_ab_ref = (a + b);//非常量右值(a + b)可以被常量左值c_ab_ref绑定
}
分析以上代码可以发现
- 非常量左值引用能绑定非常量左值
- 常量左值引用可以绑定非常量左值、常量左值、非常量右值、常量右值
右值引用
绑定到右值的引用就是右值引用,通过&&取得右值引用。
右值引用也分为常量右值引用和非常量右值引用。
#include<bits/stdc++.h>
using namespace std;
int main()
{
/* 右值引用*/
int a = 10;//a是非常量左值,有确定的内存地址,有变量名
//常量左值
const int b = 20; //b是常量左值,有确定的内存地址,有变量名
const int c = 30; //c是常量左值,有确定的内存地址,有变量名
//非常量右值
int&& a_ref = a; //a是非常量左值
int&& b_ref = b; //b是常量左值
int&& ab_ref = a+b; //a+b是非常量右值
int&& bc_ref = b+c; //b+c是常量右值,为啥可以我不知道
//常量右值
const int&& c_a_ref = a;
const int&& c_b_ref = b;
const int&& c_ab_ref = a + b;
const int&& c_bc_ref = b + c;
}
分析以上代码可知:
- 右值引用只可以绑定右值,不可以绑定左值
- 非常量右值可以绑定非常量右值
- 常量右值可以绑定非常量右值和常量右值
那么出现的一个问题是如果我想使用右值引用绑定左值怎么办呢?那就是使用std::move。std::move将左值转换为同类型的右值。
DAY 6
今天想要说一说C++中的智能指针,C++中有auto_ptr、unique_ptr和shared_ptr和weaked_ptr这几种,后来auto_ptr被C++摒弃了,只剩下了后三种。
先来讲讲为什么要有智能指针,当我们用new在堆内存中开辟了一块空间后,往往会因为疏忽大意,忘记delete,这时就会出现内存泄漏的问题。为了避免这种这种问题的出现,智能指针就被开发出来了。智能指针其实就是类似于指针的类,当智能指针的生命周期结束以后,就会自动调用析构函数,释放掉堆区内存。
auto_ptr
虽然auto_ptr被抛弃了,但是我还是要盘一下,为什么要抛弃auto_ptr,我们看下面这段代码
auto_ptr<int>ptr1(new int(10));
auto_ptr<int>ptr2;
ptr1 = ptr2;
上段代码创建了两个auto_ptr:ptr1和ptr2,两个指针指向同一块内存,会在析构的时候重复释放,运行是内存崩溃。
解决auto_ptr出现的问题的方法
- 深拷贝,重新开辟一块内存
- 所有权转移
- 引用一种新机制,使得多个指针可以指向同一块内存(后面要说的shared_ptr)
unique_ptr
顾名思义,unique_ptr是独占式思想,除了我别人不能指向这块内存。看下面这段代码
unique_ptr<int> ptr = make_unique<int>(200);
unique_ptr<int> ptr2;
//ptr2 = ptr; //not allowed
当我们企图像使用auto_ptr一样去使用unique_ptr的时候,会发现编译出错,因此unique_ptr比auto_ptr更安全,(编译错误比潜在的程序崩溃安全)。
相比于auto_ptr,unique_ptr还可以用于数组,
unique_ptr<int[]>ptr_arr = make_unique<int[]>(10);
for (int i = 0; i < 10; i++) {
ptr_arr[i] = i * i; //ptr是数组指针,指向数组,可以直接用数组名+下标的形式表达
}
for(int i = 0; i < 10; i++)
{
cout << ptr_arr[i] << endl;
}
两个unique指针之间是不是一定不能用“=” 操作呢?并不是,看看下面这段代码
//创建一个unique_ptr
unique_ptr<int>ptr;
ptr = unique_ptr<int>(new int(10)); //allowed
unique_ptr<int> ptr = make_unique<int>(200);
unique_ptr<int> ptr2;
//ptr2 = ptr; //not allowed
ptr2 = move(ptr); //左值转右值。此时右值很快就析构了
分析一下这段代码为什么可行?赋值语句的右边是右值,当ptr接管对象以后,立马就销毁了源ptr,所以这段代码是可行的。
因此我们得出了结论,程序将一个unique_ptr赋值给另一个时,如果源ptr是临时右值,编译器允许这样做,如果源ptr要存在一段时间,编译器禁止这种操作。
unique_ptr这种独占性的思想,在实际应用中不能满足所有的需求,如果实际操作中,你需要多个指针指向同一个对象时,unique_ptr就展现出了其局限性。
shared_ptr
shared_ptr的出现使得多个指针可以指向同一块内存
#include<iostream>
#include<memory>
#include<string>
int main()
{
shared_ptr<int>p1 = make_shared<int>(10); // 创建一个shared_ptr指向10的内存(简称A)
cout << p1.use_count() << endl;
shared_ptr<int>p2;
p2 = p1;
cout << p1.use_count() << endl; // 此时指向A的指针数量为2
cout << p2.use_count() << endl;
shared_ptr<int>p3;
p3 = move(p1);
cout << p1.use_count() << endl; //因为用了move将左值变成了右值,因此输出为0
cout << p2.use_count() << endl; // 2
cout << p3.use_count() << endl; // 2
}
shared_ptr是否线程安全呢?当多个线程读shared_ptr是安全的,但是如果多个线程对同一个shared_ptr进行写操作,则需要加锁。