各个内置数据类型的精度、极值所在的头文件是。
numeric_limits
cout << numeric_limits<double>::min() << endl;
2.22507e-308
cout <<-numeric_limits<double>::max() << endl;
-1.79769e+308 // 以这种形式表示负无穷大
这里还需注意的一点是,
numeric_limits<double>::min() !=
numeric_limits<double>::epsilon()
cout << numeric_limits<double>::epsilon() << endl;
2.22045e-016
msdn对epsilon()函数返回值的说明:
The function returns the difference between 1 and the smallest value greater than 1 that is representable for the data type.
也就是numeric_limits
的返回值是计算机所能判断两个同类型的数据是否相等的极限,也就是如果两个数的差值比这个epsilon
还小的话,在计算机看来两个数就是相等的。
double x = 1.;
double y = 1.+pow(10, -17);
cout << (x == y) << endl; // 1
类型转换运算符函数重载,不是一个完整的成员函数,因为其没有函数返回值,虽然在函数体
的内部,有return
表达式。
类型转换运算符函数重载有时是一些显示的调用类型转换运算符,有时的类型转换发生在一些极为隐蔽的地方:
class Fraction
{
public:
Fraction(int _numerator, int _denominator):
numerator_(_numerator), denominator_(_denominator)
{}
operator float() const
{
return static_cast<float>(numerator_)/denominator_;
}
private:
int numerator_, denominator_;
}
int main(int, char**)
{
Fraction frac(3, 5) ;
float f1 = float(frac); // 显式地调用类型转换函数
float f2 = frac; // 这条语句会隐式地调用转型函数
return 0;
}
一种更隐蔽的情况发生在类的初始化参数列表中:
class Floater
{
public:
Floater():val_(Fraction(1, 1)){}
private:
float val_;
}
int main(int, char**)
{
Floater f;
// 调用Floater的构造函数之前先进行初始化参数列表的初始化工作
return 0;
}
const char* s = "hello";
s
自然是const char*
类型,而"hello"
的类型是const char[6]
(包扩字符串常量末尾的\0
)是数组类型,而s
是其首地址,一个接受const char*
的函数自然不可以接受像"hello"
这样的字符串类型。
容器 | 插入 | 删除 | 查看 |
---|---|---|---|
deque | push_front(头插) push_back(尾插) |
pop_back pop_front |
back front |
vector | push_back | pop_back | back front |
stack | push | pop | top |
queue | push | pop | back front |
deque又称为双端队列,两端都可进行插入、删除和查看。stack的所有操作只可在一端,也就是尾端进行。
enum Color {red, yellow, blue, white, black, numColors};
默认(对第一个元素不赋初值的前提下),red == 0
, 然后依序递增,这时numColors
值为5,也就是Color
这一枚举类型包含的元素个数为5。
我们可以在任何位置对任意元素赋任意整数值,其后的元素依序增加1就是了。
#include
int main(int, char**)
{
std::string str("hello"); // 正确
std::cout << str << std::endl;
// 错误,没有与这些操作数(operand,std::string)相匹配的"<<"运算符
return 0;
}
cout
竟然不能输出string
类型,这太令人诧异了?究其原因,STL中的许多头文件(这其中就包括,Visual C++环境下)都包含std::basic_string
类的定义式,因为它们都间接地包含了
(但不要试图直接包含include
这些头文件(如本例的#include
)就可使用std::string
类,
typedef basic_string<char, char_traits<char>, allocator<char> >
string;
// string类型其实一个类模板的特化版本的类型重定义
然而,问题在于与之相关的operator<<
却定义在
头文件,你必须手动地将之包含。
所以,我们只需包含
(也即对operator<<
的包含)即可实现cout
对std::string
类型的输出:
#include
#include
int main(int, char**)
{
std::string str("hello");
std::cout << str << std::endl;
return 0;
}
以上的设置仅对Visual C++环境有效,也即在大多数的STL的头文件中,都包含了
std::basic_string
的定义式,仅通过对这些头文件的包含即可使用std::string
类,而想使用operator<<
却需手动包含头文件。在重申一遍,这些包含和依赖关系仅对Visual C++环境有效。
*this不是可修改的左值
class Widget
{
public:
void foo()
{
cout << typeid(this).name() << endl;
// this的类型为const Widget* const
}
}
this
表示的是每一个类对象的地址,类对象创建完成之后,会在内存中分配一块内存给这个对象,这块内存的地址即是this
的值,对象在内存中的位置是不会随便改变的。
vector<int> coll;
函数 | 返回值类型 |
---|---|
coll.begin() | vector vector |
coll.front() | vector vector |
coll.end() | vector vector |
coll.back() | vector vector<int>::const_reference |
我们可以定义一个简易版的vector
:
template<typename T, class Alloc = alloc>
class vector
{
public:
typedef T value_type;
typedef value_type* pointer;
typedef value_type* iterator;
typedef value_type& reference;
typedef size_t size_type;
typedef ptrdiff_t difference_type;
}
两者并没有什么不同,源码之前,了无秘密:
size_type length() const _NOEXCEPT
{ // return length of sequence
return (this->_Mysize);
}
size_type size() const _NOEXCEPT
{ // return length of sequence
return (this->_Mysize);
}
我们来理解[C++标准库]的这句话:所有容器提供的都是value语义
而非reference语义
,而我们又看到:
void push_back(const value_type& _Val);
push_back
函数接受的参数类型是reference
类型,千万不要以为操作容器,就意味着操纵的是外部元素的引用。可见push_back
函数会在内部对传递进来的引用进行拷贝。
不只对于vector
容器的push_back
成员函数如此,我们回头再看之前的哪句话,凡是容器总为传入的元素创建属于容器自己的拷贝,这也就解释了所有容器提供的都是value语义。
class Base
{
public:
// 我们想让DerivedA重写fooA,DerivedB重写B
// 第一我们不能将fooA和fooB都声明为纯虚函数,
// 否则两个派生类都需分别重写fooA和fooB
// 第二我们又不想让DerivedB对象调用fooA函数
// 让DerivedA对象调用fooB函数,这时我们可以在基类的实现中抛异常
virtual void fooA();
virtual void fooB();
virtual void foo() = 0;
}
inline void Base::fooA()
{
throw exception("cannot be called");
}
inline void Base::fooB()
{
throw exception("cannot be called");
}
class DerivedA :public Base
{
public:
void foo(){} // 纯虚函数必须重写,否则无法实例化对象
void fooA() { cout << "DerivedA::fooA()" << endl;}
}
class DerivedB :public Base
{
public:
void foo(){}
void fooB()
{
cout << "DerivedB::fooB()" << endl;
}
}
int main(int, char**)
{
DerivedA da;
da.fooA(); // "DerivedA::fooA()"
da.fooB(); // 抛异常
return 0;
}
这句话的潜台词是:非虚函数不必给出实现也可用该类进行实例化对象。
class A
{
public:
void foo1();
virtual void foo2();
}
int main(int, char**)
{
A a; // foo2是无法解析的外部符号
return 0;
}
class A
{
public:
void foo1();
virtual void foo2() {}
}
int main(int, char**)
{
A a; // 正确,可以对A进行实例化的操作
a.foo2(); // 正确,因为已给出foo2的实现
a.foo1(); // 错误,未在类中定义foo1的实现
return 0;
}
虚函数的一个重要特性:使用虚函数,系统要有一定的空间开销(内存分配),当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table,简称vtable),这是一个指针数组,存放每一个虚函数的入口地址,系统在进行动态关联时的时间开销是很少的,故,多态是高效的。
// A.hpp
class B;
class A
{...};
// A.cpp
A's somefuncs' implementation goes here
// B.hpp
class B
{ ... };
// B.cpp
B's implementation
A's implementation
// A 的某些实现有可能依靠B的某些接口
// 在A的hpp文件中,我们对B进行了前向声明
注意区分,类模板
和模板特化
的关系,类模板
不同于函数模板
(函数模板中同名不同参数(包括顺序和个数)构成重载关系),最本质的不同在于类模板
没有类型推导机制。如果想以同名的形式出现,只能是一种作为另外一种的特化版本出现。
我们以一个辅助类(模板参数列表中最大的类型所占字节)为例进行说明:
template<typename F, tyepname... FS>
struct variant_helper
{
static const size_t size = sizeof(F) > variant_helper::size ? sizeof(F):variant_helper::size;
};
// 错误,同名的类模板,而非模板特化
template<typename T>
struct variant_helper
{
static const size_t size = sizeof(T);
}
// 正确,以模板特化的形式出现
template<typename T>
struct variant_helper
{
static const size_t size = sizeof(T);
}
我们再来看一个相似的例子,但用到的技术是函数模板
的重载技术:
// 可变类型在的地方必须用`...`显式地告诉编译器,这是可变长参数
// 不论是声明还是其他什么地方
template<typename T, typename... Types>
void print(const T& firstArg, const Types&... types)
{
cout << firstArg << endl;
print(types...);
}
// 同名,函数重载
template<typename T>
void print(const T& arg)
{
cout << arg << endl;
}
int i = getInt(); // i: 左值,getInt()返回一个匿名的右值
int&& i = getInt(); // getInt()返回的匿名右值没有销毁,而是被右值引用所捕获,所绑定
注意在一个类(而非对象)的内部,可以访问任何私有成员,哪怕是作为参数传递进来的另外一个对象:
class A
{
public:
A():m_ptr(new int(0)){}
A(const A& a):m_ptr(new int(*a.m_ptr)){}
// 可以访问a的私有成员
private:
int* m_ptr;
}
带有堆内存(作为其成员变量)的类,必须提供一个深拷贝拷贝构造函数,默认的拷贝构造函数是浅拷贝,会发生指针悬挂(dangling pointer)的问题。
class A
{
public:
A():m_ptr(new int(0)) {}
A(const A& a):m_ptr(new int(*a.m_ptr)){}
// 深拷贝拷贝构造函数
~A() {delete m_ptr;}
private:
int* m_ptr;
}
A getA()
{
return A();
}
int main(int, char**)
{
A a = getA();
return 0;
}
如果不提供深拷贝拷贝构造函数,上述代码将会发生编译错误,内部的m_ptr
将会被删除两次,一次发生在getA()函数内部,临时右值析构的时候删除一次,第二次main函数中局部对象a析构时释放一次,因为是浅拷贝,这两个对象的m_ptr是同一个指针,这就是所谓指针悬挂的问题。如果不提供深拷贝拷贝构造函数的话,A b(a);
, 此时操作b的m_ptr指针也一并操作了a对象的m_ptr。
int* p1 = new int(10);
int* p2 = p1;
delete p1;
cout << *p2 << endl; // 此时发生指针悬挂的问题
转换指针所有权:
int* p1 = new int(10);
int* p2 = p1;
p1 = nullptr;
cout << *p2 << endl; // 正确值
[1] numeric_limits::epsilon
[2] Why cannot cout a string