C++中的小知识
nullptr
nullptr 出现的目的是为了替代 NULL。为啥要替代呢? 我们看一下C++是如何定义NULL的!
很显眼c++是NULL定义成了 0 , 我们在C语言经常这样做
char *str=NULL;
在C语言,NULL被定义成 ((void*)0), 并且C 允许直接将 void * 隐式转换到其他类型 ,这里 NULL会被强制转换成((char*)0)。但是C++中是不允许直接将 void * 隐式转换到其他类型,如果有如下情景!
void foo(char*);
void foo(int);
那么 foo(NULL); 这个语句将会去调用 foo(int),从而导致代码违反直觉。
为了解决这个问题,C++11 引入了 nullptr 关键字,专门用来区分空指针、0。而 nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
类型推导
在传统 C 和 C++ 中,参数的类型都必须明确定义,这其实对我们快速进行编码没有任何帮助,尤其是当我们面对一大堆复杂的模板类型时,必须明确的指出变量的类型才能进行后续的编码,这不仅拖慢我们的开发效率,也让代码变得又臭又长。
C+ +11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。这使得 C+ + 也具有了和其他现代编程语言一样,某种意义上提供了无需操心变量类型的使用习惯。
auto
如果我们为 vector 声明一个常量迭代器一般要这么写std::vector
vector<int>::const_iterator it = vec.cbegin();
但是这里注意的是auto声明的变量一定要初始化,这样auto才能推到出变量类型!毕竟auto没有让c++从强语法类型变为弱语法类型.
同时也要额外记住如下两点
- auto 不能用于函数传参,因此下面的做法是无法通过编译的
- auto 还不能用于推导数组类型:
decltype
decltype可以作为auto一个补充,如果我们有如下的场景,
int a=5;
int c=5;
//decltype(表达式)
decltype(a+b) c;
这里c就是int类型,个人感觉没有auto常用。
类型别名模板 using
模板是用来产生类型的,在传统 C++ 中,typedef 可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。因为,模板不是类型。
例如下面的语法是错误的!!
template<typename T>
typedef MagicType<std::vector<T>, std::string> FakeDarkMagic;
那么我们在写模板类的时候不能一直这么恶心地写那么长的名字吧
通常我们使用 typedef 定义别名的语法是:typedef 原名称 新名称;,但是对函数指针等别名的定义语法却不相同,这通常给直接阅读造成了一定程度的困难。
这里我们using登场,语法如下
using newname = oldname;
例如
template<typename T>
using iterator =_list_iterator<T, T&, T*> ;
引用和const 秘诀
const 修饰参数引用
将引用用好,我们极大可以优化程序的效率!对比如下两端代码
template<typename T>
void insert(iterator it,T data)
template<typename T>
void insert(const iterator& it,const T& data);
对比起来,第二段代码允许效率比第一段效率高。
引用,可以看作指针,我们传入 指针的效率和传入对象效率那可不是一样的,传入对象c++会默认调用拷贝构造函数,如果是很复杂的类,每一次调用,就要调用复杂的拷贝构造函数。
当然,传入引用,在我们阅读和理解代码来说,大概率会修改里面的引用的值,但是在这里我们无需修改呢?当然使用const来修饰一下,表面这个参数是无法修改的。
总结一下:const防止传入的参数代表的内容在函数体内被改变,但仅对指针和引用有意义。因为如果是按值传递,传给参数的仅仅是实参的副本,即使在函数体内改变了形参,实参也不会得到影响。
const 修饰类的方法
int empty()const;
很多类的方法,不会修改类成员!就比如判断链表是否为空,我们完全无需修改类的成员。这里我们一定在末尾加上const,来修饰整个函数(当然这里只对类的成员函数有效,普通函数这样是语法错误的)。 加入const一来,增强了我们代码的可读性,二来c++规定 const函数不能调用非const函数 。
也就说当程序员A封装了某一个类或者接口,没有注意这个const细节,当程序员B去再对程序员A所写的类或者接口再次封装的时候采用const修饰了函数,发现编译不过去了。所以养成良好的习惯,正确的使用const.
const修饰返回值
用const来修饰返回的指针或引用,保护指针指向的内容或引用的内容不被修改,也常用于运算符重载。归根究底就是 使得函数调用表达式不能作为左值。
委托构造函数
C++11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:
#include <iostream>
class Base {
public:
int value1;
int value2;
Base() {
value1 = 1;
}
Base(int value) : Base() // 委托 Base() 构造函数
{
value2 = value;
}
};
int main() {
Base b(2);
std::cout << b.value1 << std::endl;
std::cout << b.value2 << std::endl;
}
强制枚举类型
在传统 C++中,枚举类型并非类型安全,枚举类型会被视作整数,则会让两种完全不同的枚举类型可以进行直接的比较(虽然编译器给出了检查,但并非所有),甚至同一个命名空间中的不同枚举类型的枚举值名字不能相同,这通常不是我们希望看到的结果。
C++11 引入了枚举类(enumeration class),并使用 enum class 的语法进行声明:
enum class new_enum : unsigned int {
value1,
value2,
value3 = 100,
value4 = 100
};
这样定义的枚举实现了类型安全,首先他不能够被隐式的转换为整数,同时也不能够将其与整数数字进行比较, 更不可能对不同的枚举类型的枚举值进行比较。但相同枚举值之间如果指定的值相同,那么可以进行比较:
if (new_enum::value3 == new_enum::value4) {
// 会输出
std::cout << "new_enum::value3 == new_enum::value4" << std::endl;
}
C/C++ 可变参数函数
C语言 做法
函数,我们经常是无法知道参数的个数和类型的,就最简单的printf() 来说,函数原型如下
int printf(const char *format, ...);
这里是如何做到参数可变的呢?
这里的原理,等深入学习之后,我在来做笔记吧,涉及一点汇编知识,目前掌握如何处理不定参数即可。
需要引入头文件 #include <stdarg.h>
定义不定参数函数,要用到下面这些宏:
- va_start(ap, count): 初始化一个va_list变量ap,count是不定参数的个数
- va_arg(ap, type): 获取(下)一个type类型的参数
- va_end(ap): 结束使用ap
如下,求出cnt个数的和的实现
#include <stdarg.h>
int sum(int cnt,...) {
int sum = 0;
int i;
va_list ap;
va_start(ap, cnt);
for(i = 0; i < cnt; ++i)
sum += va_arg(ap, int);
va_end(ap);
return sum;
}
C++ 类别安全的变长参数函数
用起来其实比上述方法更简单!!例如:
template<typename... Args> void printf(const std::string &str, Args... args);
那么我们定义了变长的模板参数,如何对参数进行解包呢?
首先,我们可以使用 sizeof... 来计算参数的个数,然后我们可以选择采用递归的方式去访问每一个参数,如下
#include <iostream>
template<typename T0>
void printf1(T0 value) {
std::cout << value << std::endl;
}
template<typename T, typename... Ts>
void printf1(T value, Ts... args) {
std::cout << value << std::endl;
printf1(args...);
}
int main() {
printf1(1, 2, "123", 1.1);
return 0;
}