C++基础——一些细节、常犯错误的汇总

  • 细节 1numeric_limits doublemin
  • 细节 2类型转换运算符函数重载的调用时机
  • 细节 3const char s hello world
  • 细节 5dequevectorstackqueue
  • 细节 6enum类型元素的值
  • 细节 7为什么不能输出cout一个string
  • 细节 8this表达式必须是可修改的左值
  • 细节 9collbegin与collfront
  • 细节 10 strlength strsize
  • 细节 11push_backconst value_type
  • 细节 12类中非纯虚的函数必须给出实现
  • 细节 13虚函数必须给出implementation否则无法进行实例化
  • 细节 14implementation的位置
  • 细节 15同一文件中不可有同名的类模板
  • 细节 16int i getInt 与 int i getInt
  • 细节 17私有成员的访问时机
  • 细节 18带有堆内存的类
  • 细节 19简单转换一个指针的所有权
  • Reference

细节 1:numeric_limits ::min()

各个内置数据类型的精度、极值所在的头文件是

numeric_limits::min()表示的是无穷小量(接近0),而不是

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()的返回值是计算机所能判断两个同类型的数据是否相等的极限,也就是如果两个数的差值比这个epsilon还小的话,在计算机看来两个数就是相等的。

double x = 1.;
double y = 1.+pow(10, -17);
cout << (x == y) << endl;   // 1

细节 2:类型转换运算符函数重载的调用时机

类型转换运算符函数重载,不是一个完整的成员函数,因为其没有函数返回值,虽然在函数体的内部,有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;
}

细节 3:const char* s = “hello world!”

const char* s = "hello";

s自然是const char*类型,而"hello"的类型是const char[6](包扩字符串常量末尾的\0)是数组类型,而s是其首地址,一个接受const char*的函数自然不可以接受像"hello"这样的字符串类型。

细节 5:deque、vector、stack、queue

容器 插入 删除 查看
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的所有操作只可在一端,也就是尾端进行。

细节 6:enum类型元素的值

enum Color {red, yellow, blue, white, black, numColors};

默认(对第一个元素不赋初值的前提下),red == 0, 然后依序递增,这时numColors值为5,也就是Color这一枚举类型包含的元素个数为5。
我们可以在任何位置对任意元素赋任意整数值,其后的元素依序增加1就是了。

细节 7:为什么不能输出cout一个string?

#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<<的包含)即可实现coutstd::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++环境有效。

细节 8:*this:表达式必须是可修改的左值

*this不是可修改的左值

class Widget
{
public:
    void foo()
    {
        cout << typeid(this).name() << endl;               
            // this的类型为const Widget* const
    }
}

this表示的是每一个类对象的地址,类对象创建完成之后,会在内存中分配一块内存给这个对象,这块内存的地址即是this的值,对象在内存中的位置是不会随便改变的。

细节 9:coll.begin()与coll.front()

vector<int> coll;
函数 返回值类型
coll.begin() vector::iterator
vector::const_iterator
coll.front() vector::reference
vector::const_reference
coll.end() vector::iterator
vector::const_iterator
coll.back() vector::reference
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;
}

细节 10: str.length() str.size()

两者并没有什么不同,源码之前,了无秘密:

size_type length() const _NOEXCEPT
    {   // return length of sequence
    return (this->_Mysize);
    }

size_type size() const _NOEXCEPT
    {   // return length of sequence
    return (this->_Mysize);
    }

细节 11:push_back(const value_type&)

我们来理解[C++标准库]的这句话:所有容器提供的都是value语义而非reference语义,而我们又看到:

void push_back(const value_type& _Val);

push_back函数接受的参数类型是reference类型,千万不要以为操作容器,就意味着操纵的是外部元素的引用。可见push_back函数会在内部对传递进来的引用进行拷贝。

不只对于vector容器的push_back成员函数如此,我们回头再看之前的哪句话,凡是容器总为传入的元素创建属于容器自己的拷贝,这也就解释了所有容器提供的都是value语义

细节 12:类中非纯虚的函数必须给出实现

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

细节 13:虚函数必须给出implementation,否则无法进行实例化

这句话的潜台词是:非虚函数不必给出实现也可用该类进行实例化对象。

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),这是一个指针数组,存放每一个虚函数的入口地址,系统在进行动态关联时的时间开销是很少的,故,多态是高效的。

细节 14:implementation的位置

// 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进行了前向声明

细节 15:同一文件中不可有同名的类模板

注意区分,类模板模板特化的关系,类模板不同于函数模板(函数模板中同名不同参数(包括顺序和个数)构成重载关系),最本质的不同在于类模板没有类型推导机制。如果想以同名的形式出现,只能是一种作为另外一种的特化版本出现。

我们以一个辅助类(模板参数列表中最大的类型所占字节)为例进行说明:

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

细节 16:int i = getInt(); 与 int&& i = getInt();

int i = getInt();   // i: 左值,getInt()返回一个匿名的右值
int&& i = getInt(); // getInt()返回的匿名右值没有销毁,而是被右值引用所捕获,所绑定

细节 17:私有成员的访问时机

注意在一个类(而非对象)的内部,可以访问任何私有成员,哪怕是作为参数传递进来的另外一个对象:

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

细节 18:带有堆内存的类

带有堆内存(作为其成员变量)的类,必须提供一个深拷贝拷贝构造函数,默认的拷贝构造函数是浅拷贝,会发生指针悬挂(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。

细节 19:简单转换一个指针的所有权

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;    // 正确值

Reference

[1] numeric_limits::epsilon
[2] Why cannot cout a string

你可能感兴趣的:(C/C++)