【C++11 —— 常用语法】

C++11简介

在2003年,C++标准委员会提交了一份技术勘误表(简称TC1),使得C++03这个名称取代了C++98,成为C++11之前的最新C++标准名称。不过,由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分没有改动,因此人们习惯性地将这两个标准合并称为C++98/03标准

C++0xC++11,C++标准经历了十年的磨砺,第二个真正意义上的标准终于问世。相比于C++98/03C++11带来了数量可观的变化,包含约140个新特性,并修正了C++03标准中约600个缺陷,使得C++11更像是从C++98/03中孕育出的一种新语言。

相比较而言,C++11能够更好地用于系统开发库开发,语法更加丰富和简单化,且更加稳定和安全。不仅功能更强大,还能提升程序员的开发效率,因此在公司实际项目开发中也得到了广泛应用,成为学习的重点。

C++11增加的语法特性非常多,篇幅较大,无法一一讲解。本节课程将主要讲解实际中比较实用的语法特性

小故事:C++标准的演变
1998年是C++标准委员会成立的第一年,最初计划每5年视实际需要更新一次标准。在研究C++03的下一个版本时,委员会一开始计划在2007年发布,因此最初这个标准被称为C++07。然而,到2006年时,委员会意识到2007年肯定无法完成C++07的发布,甚至2008年也可能无法完成。最终,他们决定将其称为C++0x,其中"x"表示不确定具体的完成年份。
结果,2010年时仍未完成,最终在2011年,C++标准终于完成。因此,这个标准被正式命名为C++11。

统一的列表初始化

{}初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:

struct Test
{
    int a; // 成员变量a
    int b; // 成员变量b
};

int main()
{
    // 1. 使用初始化列表初始化整型数组
    int arr1[] = { 1, 2, 3, 4 }; // 数组大小由编译器自动推导为4

    // 2. 使用初始化列表初始化整型数组并设置默认值
    int arr2[5] = { 0 }; // 数组大小为5,所有元素初始化为0

    // 3. 使用初始化列表初始化结构体变量
    Test t = { 1, 2 }; // 结构体t的成员a初始化为1,b初始化为2

    return 0;
}

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自
定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

#include <iostream>

// 定义一个结构体Point,包含两个整型成员变量_x和_y
struct Point
{
    int _x; // x坐标
    int _y; // y坐标
};

int main()
{
    // 1. 使用普通赋值初始化整型变量x1
    int x1 = 1; // x1被初始化为1

    // 2. 使用统一初始化(C++11引入)初始化整型变量x2
    int x2{ 2 }; // x2被初始化为2

    // 3. 使用初始化列表初始化整型数组array1
    int array1[]{ 1, 2, 3, 4, 5 }; // array1的大小由编译器自动推导为5,元素分别为1, 2, 3, 4, 5

    // 4. 使用初始化列表初始化整型数组array2,所有元素初始化为0
    int array2[5]{ 0 }; // array2的大小为5,第一元素为0,其余元素自动初始化为0

    // 5. 使用初始化列表初始化结构体Point的对象p
    Point p{ 1, 2 }; // p的成员_x被初始化为1,_y被初始化为2

    // 6. 使用new表达式动态分配一个大小为4的整型数组,并初始化所有元素为0
    // C++98中,不能使用初始化列表,必须手动初始化
    // int* pa = new int[4]; // C++98: 只分配内存,元素未初始化
    // for (int i = 0; i < 4; ++i) pa[i] = 0; // 手动初始化为0

    // C++11中,支持使用初始化列表进行初始化
    int* pa = new int[4]{ 0 }; // pa指向动态分配的数组,所有元素初始化为0

    // 7. 释放动态分配的内存(避免内存泄漏)
    delete[] pa; // 使用delete[]释放数组

    return 0; // 返回0表示程序正常结束
}

创建对象时也可以使用列表初始化方式调用构造函数初始化。比如:


// 定义一个类A
class A
{
public:
    // 构造函数,接受一个int参数x,初始化_x和_y为x
    A(int x)
        : _x(x), _y(x) // 使用初始化列表初始化成员变量
    {}

    // 构造函数,接受两个int参数x和y,分别初始化_x和_y
    A(int x, int y)
        : _x(x), _y(y) // 使用初始化列表初始化成员变量
    {}

private:
    int _x; // 私有成员变量_x
    int _y; // 私有成员变量_y
};

int main()
{
    // 1. 使用统一初始化(C++11引入)初始化对象a1,调用A(int x)构造函数
    A a1{ 1 }; // a1的_x和_y都被初始化为1

    // 2. 使用赋值初始化方式初始化对象a2,调用A(int x)构造函数
    A a2 = { 2 }; // a2的_x和_y都被初始化为2

    // 3. 使用统一初始化初始化对象a3,调用A(int x, int y)构造函数
    A a3{ 1, 2 }; // a3的_x被初始化为1,_y被初始化为2

    // 4. 使用赋值初始化方式初始化对象a4,调用A(int x, int y)构造函数
    A a4 = { 1, 3 }; // a4的_x被初始化为1,_y被初始化为3

    // 5. 使用统一初始化创建一个临时对象,并绑定到常量引用aa3
    const A& aa3 = { 2, 2 }; // 创建一个临时对象,_x和_y都被初始化为2,aa3引用这个临时对象

    return 0; // 返回0表示程序正常结束
}

注: 带有 = 的初始化通常会创建临时对象并进行拷贝赋值,而不带 = 的初始化直接调用构造函数创建对象,效率更高。

std::initializer_list

类型定义:
std::initializer_list<T> 是一个模板类,其中 T 是元素的类型。它提供对一组 const T 对象的访问。
构造与初始化:
 当你使用大括号初始化时,例如 X x{ 1, 2, 3 };,编译器会自动创建一个 std::initializer_list 对象,包含这些元素。
成员函数:

  • begin()end():返回指向元素的迭代器。
  • size():返回元素的数量。

使用场景:
std::initializer_list 常用于容器的初始化、构造函数参数、以及在需要传递多个值时。

代码示例:

#include <iostream>      // 引入输入输出流库
#include <initializer_list> // 引入initializer_list支持

using namespace std; // 使用标准命名空间

// 定义一个类模板Test
template <typename T>
class Test
{
public:
    // 构造函数,接受一个initializer_list<T>类型的参数
    Test(initializer_list<T> list)
    {
        // 遍历initializer_list中的每个元素
        for (const auto& e : list)
        {
            cout << e << " "; // 输出每个元素
        }
        cout << "\nSize: " << list.size() << endl; // 输出元素的数量
    }
};

int main()
{
    // 创建Test<int>类型的对象t1,并使用initializer_list初始化
    Test<int> t1{ 1, 2, 3, 4 };
	
	//1 2 3 4 
	//Size: 4
	
    return 0; // 返回0表示程序正常结束
}


上述示例展示了如何使用 initializer_list 在类模板中接收一组初始化值,方便地使用大括号来初始化一些值,并直接通过构造函数进行处理。

让模拟实现的vector也支持{}初始化和赋值


#pragma once
#include<assert.h>
#include <initializer_list>
using namespace std;

namespace qq
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}
		const_iterator end() const
		{
			return _finish;
		}

		vector():_start(nullptr),_finish(nullptr),_endofstorage(nullptr)
		{}

		vector(const vector<T>& v)
		{
			reserve(v.capacity());
			for (auto& e : v)
			{
				push_back(e);
			}
		}

		vector(std::initializer_list<T> l)
		{
			/*_start = new T[l.size()];
			_finish = _endofstorage = _start + l.size();*/
			reserve(l.size());

			iterator vit = _start;
			typename initializer_list<T>::iterator lit = l.begin();
			while (lit != l.end())
			{
				*vit++ = *lit++;
			}
			_endofstorage = _finish = _start + l.size();
		}

		~vector()
		{
			delete[] _start;
			_start = _finish = _endofstorage = nullptr;
		}

		size_t size()const
		{
			return _finish - _start;
		}

		size_t capacity() const
		{
			return _endofstorage - _start;
		}

		T& operator[](size_t pos)
		{
			assert(pos < size());

			return _start[pos];
		}

		void reserve(size_t n)
		{
			//if (capacity() == 0)	//当为空数组时,专门为initializer_list服务。
			//{
			//	T* tmp = new T[n];
			//	size_t old_size = size();
			//	memcpy(tmp, _start, old_size * sizeof(T));
			//	delete[] _start;

			//	_start = tmp;
			//	_finish = _endofstorage = tmp + n;
			//}

			if (n > capacity())
			{
				T* tmp = new T[n];
				size_t old_size = size();
				memcpy(tmp, _start, old_size * sizeof(T));
				delete[] _start;

				_start = tmp;
				_finish = tmp + old_size;
				_endofstorage = tmp + n;
			}
		}

		void resize(size_t n, const T& val = T())
		{
			if (n > size())
			{
				reserve(n);
				//插入
				while (_finish < _start + n)
				{
					*_finish = val;
					_finish++;
				}
			}
			else
			{
				//删除
				_finish = _start + n;
			}
		}

		void push_back(const T& val)
		{
			if (_finish == _endofstorage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}
			*_finish = val;
			_finish++;
		}

		void pop_back()
		{
			assert(!empty());
			_finish--;
		}

		bool empty()
		{
			//return  _start == nullptr;
			return size() == 0;
		}

		void insert(iterator pos, const T& val)
		{
			assert(pos >= _start);
			assert(pos <= _finish);


			size_t n = pos - _start;
			reserve(size()+1);
			pos = _start + n;			//一定要更新pos的位置

			iterator it = _finish - 1;
			while (it >= pos)
			{
				*(it + 1) = *(it);
				it--;
			}
			*pos = val;
			_finish++;
		}

		void erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);

			/*iterator it = pos ;
			while (it < _finish -1)
			{
				*it = *(it + 1);
				it++;
			}
			_finish--;*/

			iterator it = pos + 1;
			while (it < _finish)
			{
				*(it - 1) = *(it);
				it++;
			}
			_finish--;
		}

		void print_Vector()
		{
			for (auto e : *this)
			{
				std::cout << e << " ";
			}
			std::cout << std::endl;

		}
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _endofstorage = nullptr;
	};
}

注意:initializer_list中复用reserve进行扩容时,因为此时数组为空,所以需要特殊化处理_finish 与_endofstorage 。


为什么typename initializer_list<T>::iterator lit = l.begin(); 要用 typename ?
答:因为typename initializer_list<T>::iterator 是依赖名称,在模板中,当一个名称依赖于模板参数的时候,编译器无法确定它是一个非类型,这种情况下,编译器需要你明确的告诉他是一个类型,此时就需要使用typename来声明。

声明

c++11提供了多种简化声明的方式,尤其是在使用模板时。

auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

代码示例:

int main()
{
    // 声明整型变量i并初始化为10
    int i = 10;
    
    // 声明指向int类型的指针p,并让p指向i
    auto p = &i;
    
    // 声明函数指针pf,并让其指向strcpy函数
    auto pf = strcpy;
    
    // 输出指针p的类型名称
    cout << typeid(p).name() << endl;
    
    // 输出函数指针pf的类型名称
    cout << typeid(pf).name() << endl;
    
    // 创建一个string到string的映射dict,并初始化一些键值对
    map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
    
    // 使用auto声明迭代器it,并让其指向dict的起始位置
    auto it = dict.begin();
    
    return 0;
}

decltype

关键字decltype将变量的类型声明为表达式指定的类型。

#include <iostream>  // 包含输入输出流库
#include <typeinfo>  // 包含typeid用于获取类型信息

using namespace std;

// 模板函数F,接受两个参数t1和t2
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
    // 使用decltype推导t1和t2相乘的结果类型,并声明ret
    decltype(t1 * t2) ret;
    // 输出ret的类型名称
    cout << typeid(ret).name() << endl;
}

int main()
{
    const int x = 1;           // 声明一个常量整型变量x,值为1
    double y = 2.2;            // 声明一个双精度浮点型变量y,值为2.2
    
    // 使用decltype推导x和y相乘的结果类型,并声明ret
    decltype(x * y) ret;      // ret的类型是double
    // 使用decltype推导x的地址类型,并声明p
    decltype(&x) p;           // p的类型是int*
    
    // 输出ret的类型名称
    cout << typeid(ret).name() << endl;
    // 输出p的类型名称
    cout << typeid(p).name() << endl;
    
    // 调用模板函数F,传入整型1和字符'a'
    F(1, 'a');
    
    return 0;
}

这段代码展示了C++中的decltype和模板函数的使用。decltype用于在编译时推导表达式的类型,例如,decltype(x * y)推导出ret的类型为double,而decltype(&x)推导出p的类型为int*。模板函数F接受两个参数,编译器根据传入的参数自动推导模板参数类型,并使用decltype推导其乘积的类型。通过typeid,可以输出变量的实际类型名称,增强了代码的灵活性与可读性。

nullptr

C++11引入nullptr的主要原因是为了解决在C++98/03中使用NULL作为空指针常量时可能出现的问题。

C++98/03中的问题:

  1. 重载函数的歧义
    当有重载函数时,使用NULL作为参数可能会导致调用歧义。例如:

    void foo(int);
    void foo(char*);
    
    foo(NULL); // 调用foo(int),而不是foo(char*)
    
    
    
  2. 隐式类型转换:
    NULL可以隐式转换为整型,这可能会导致一些意料之外的行为。

  3. 可读性:
    使用0作为空指针常量可能会降低代码的可读性,因为不清楚它是表示整数0还是空指针。

nullptr的特点:
为了解决这些问题,C++11引入了nullptr关键字。nullptr具有以下特点:

  1. 类型为std::nullptr_t:
    nullptr 的类型为std::nullptr_t,这是一个独立的类型。
  2. 可以隐式转换为指针类型:
    nullptr可以隐式转换为任何指针类型或指向成员类型。
  3. 不能隐式转换为整型:
    nullptr不能隐式转换为整型,除了bool类型。
  4. 不会导致重载函数的歧义:
    使用nullptr调用重载函数时不会产生歧义。

范围for

C++11引入的 范围for循环(Range-based for loop) 是一种简化遍历容器和数组的语法,使代码更加简洁和易于理解。其基本语法格式为:

for (元素类型 变量名 : 可迭代对象) {
    // 循环体
}

主要特点:

  1. 简洁性:范围for循环避免了传统for循环中需要手动管理索引或迭代器的繁琐,使代码更加清晰。例如,遍历一个std::vector时,不再需要显式调用begin()和end()。
  2. 安全性:由于不直接操作索引或迭代器,范围for循环减少了因索引越界或迭代器失效导致的问题。
  3. 通用性:适用于任何定义了begin()和end()方法的类型,包括标准库容器、数组和自定义容器。

使用示例:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec{1, 2, 3, 4, 5};
    
    // 使用范围for循环遍历vector
    for (auto num : vec) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

注意事项:

  • 只读访问:默认情况下,范围for循环对元素的访问是只读的。如果需要修改元素,必须将循环变量声明为引用类型,例如for (auto& item : container)。
  • 不能修改容器大小:在遍历过程中,不能通过添加或删除元素来修改容器的大小,因为这可能导致迭代器失效。

范围for循环极大地提高了C++代码的可读性和可维护性,是C++11中一个非常实用的特性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值