每日一点小知识

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;
}

引用做函数返回值:

  1. 不能返回局部变量的引用,因为局部变量存放在栈区,编译器自主释放,函数执行结束后,局部变量的内存空间会释放,会导致引用失败。
  2. 函数的调用可以作为左值
int& test01()
{
	static int a = 10; //声明成静态变量,避免内存释放的问题
	return a;
}
int main()
{
	int &b = test01();
	cout << "b=" << b<<endl;
	test01() = 1000; // 如果函数的返回值是引用,函数就可以作为左值
	cout << "b=" << b<<endl;
}

常量引用:

作用:让函数传递信息,而不对信息做修改,同时向使用引用,应该使用常量引用。

方式:在函数头和函数原型中加const。

引用和指针的区别:

经常会被问到引用和指针的区别,在这里总结一下:

  1. 引用是给变量取别名,实际上是和变量一样东西,但是指针存储的是地址。
  2. 引用必须初始化,而且初始化后不可变动,但是指针不需要,指针也可以指向别的地址。
  3. 引用不可以为NULL,但是指针可以。
  4. 可以有多级指针,但是没有多级引用。
  5. 自增的结果不一样。

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绑定
}

分析以上代码可以发现

  1. 非常量左值引用能绑定非常量左值
  2. 常量左值引用可以绑定非常量左值、常量左值、非常量右值、常量右值

右值引用

绑定到右值的引用就是右值引用,通过&&取得右值引用。

右值引用也分为常量右值引用和非常量右值引用。

#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;
}

分析以上代码可知:

  1. 右值引用只可以绑定右值,不可以绑定左值
  2. 非常量右值可以绑定非常量右值
  3. 常量右值可以绑定非常量右值和常量右值

那么出现的一个问题是如果我想使用右值引用绑定左值怎么办呢?那就是使用std::move。std::move将左值转换为同类型的右值。

DAY 6

今天想要说一说C++中的智能指针,C++中有auto_ptrunique_ptrshared_ptrweaked_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进行写操作,则需要加锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值