前言
本篇文章主要记录在学习侯捷老师的C++课程过程中遇到的此前没了解过的知识,并不涵盖所有知识点。理解也不完全正确,如有错误请多多包涵。
guard(防御式声明)
#ifndef __COMPLEX__
#define __COMPLEX__
...
#endif在写一个头文件的时候,可以用这样的防御式声明,作用是在引用这个头文件时,如果没有定义(ifndef)__COMPLEX__,那么定义(define)__COMPLEX__,然后进入到头文件里的内容,最后(endif)。如果程序第二次引用这个头文件,因为此前已经定义过__COMPLEX__,那么就不会进入到头文件里。
class template(模板) 
 
template<typename T>
class complex
{
public:
    complex (T r = 0, T i = 0)
        : re (r), im (i)
    {}
    ...
private:
    T re, im;
    ...
};
{
    complex<double> c1(2.5, 1.5);
    complex<int> c2(2,6);
    ...
}如果不使用模板,定义类的时候,使用不同的类型声明成员变量就要重复写多个类(比如分别用double和int声明re,im),这么做是很冗余的,因此可以使用类模板(template)。在把类实例化成对象时,可以使用<double>或<int>分别指定这个类的T是什么类型。
inline(内联)函数
函数若在class内定义完成,那么成为inline(内联)函数候选,inline会更快,所以理论上把所有的函数都定义成inline是最好的,但是并不是所有的函数都能被定义成inline,太过复杂的函数是不会被定义成inline的(这里的分界点老师没有提)。所以即使写代码的时候,所有函数都写成inline的形式,那只是程序员对编译器的期望,实际上一个函数会不会被定义成inline,是由编译器自己决定的。
上网查了一下,inline之所以快是因为避免了函数调用所带来的保存现场,变量压栈出栈,跳转新函数,存储函数返回值,执行完返回现场等开销,从而使得程序运行变快。
即使不在class内定义函数,依然可以让编译器知道我们希望这个函数为inline,代码如下:
inline double
imag(const complex& x)
{
    return x.imag();
}constructor(构造函数)
class complex
{
public:
    //1
    complex (double r = 0, double i = 0)
        : re (r), im(i)
    {}
    //2
    complex (double r = 0, double i = 0)
    { re = r; im = i; }
private:
    double re, im;
}常见的构造函数有以上两种写法,其中第1种比第2种更好,效率更高,尽管它们结果是相同的。因为构造函数中的变量会经过两个阶段:初始化和赋值。初始化即是第1种的第二行,所以第1种方式在初始化阶段就已经完成了对re和im变量的改变。第2种方法没有利用到初始化这一步,是在赋值阶段才完成对re和im变量的改变,比第1种方法慢了一步。
构造函数放在private区
构造函数是可以放在private区里的,表示外界不能够通过构造函数实例化对象,老师举了一个单例的例子:
class A {
public:
    static A& getInstance();
    setup() {...}
private:
    A();
    A(const A& rhs);
    ...
};
A& A::getInstance()
{
    static A a;
    return a;
}
A::getInstance().setup();目前很多语法看不懂,之后再回来补。
const member functions(常量成员变量)
class complex
{
public:
    complex (double r = 0, double i = 0)
         : re (r), im (i)
    {}
    double real () const { return re; }
    double imag () const { return im; }
private:
    double re, im;
};
{
    const complex c1(2,1);
    cout << c1.real();
    cout << c1.imag();
}定义函数时,如果这个函数不涉及类中成员变量的改变,那么一定要在小括号和大括号之间加上const(尽管有时候不加不一定会出错)。
原因是,加入定义函数时不加const(这表明这个函数有可能会对成员变量做出修改),使用者在实例化对象时使用了const(这表明使用者实例化了一个常量对象,即他不希望改变成员变量),那么使用者在调用对象中没有加const的函数时,编译器会报错,因为这是冲突的:使用者不希望改变成员变量,但是在定义函数时没有加const,意味着有可能会改变成员变量。
参数传递(pass by value和pass by reference(to const))
函数传递参数时,尽量传引用,因为传引用的速度跟传地址一样快,当要传的值很长,比如字符串,几百个字节,这时候传引用会快很多。但是传引用时,如果不想改变变量本身,那么需要加const,当函数对引用做出修改时,编译器就会报错。同理,函数返回的时候,可以的话,也尽量返回引用。
friend(友元)
class complex
{
public:
    ...
private:
    double re, im;
    friend complex& __doapl (complex*, const complex&);
};
inline complex&
__doapl (complex* ths, const complex& r)
{
    ths->re += r.re;
    ths->im += r.im;
    return *ths;
}一般来说,外界是无法直接访问一个类的私有成员的,但是有例外,如果一个类声明了一个函数是它的friend(在函数声明前加),那么这个函数就可以直接访问这个类实例化的对象的私有成员。
相同class的各个objects互为friends
class complex
{
public:
    ...
    
    int func(const complex& param)
    { return param.re + param.im; }
private:
    double re, im;
};
{
    complex c1(2,1);
    complex c2;
    c2.func(c1);
}这里c2将c1作为参数传进func,看类中的实现,c2直接访问了c1的私有成员,按理来说,c2相对c1来说是外界的东西,应该不能直接访问它的私有成员,但是它们都属于complex这个类,相同class的各个objects互为友元,因此c2能够直接访问c1的私有成员。
什么情况下可以返回引用,什么情况下不可以返回引用
int& returnByReference(int*a, int& b)
{
    *a += b;
    return *a;
}这种情况可以返回引用。
int returnByValue(int& a)
{
    int c = 0;
    c += a;
    return c;
}这种情况不可以返回引用。
两种情况的区别是,c是在函数内创建的,如果返回引用,虽然可以把引用传出去,但是函数结束后c就被销毁了,外部拿着传出来的引用是找不到c的。
此外,传引用相比传指针还有一个好处,就是传递者不需要知道接收者是以reference形式接收,即函数要求返回引用或者变量类型,返回的时候都可以是该变量类型;而返回指针需要知道函数要求返回指针才能这么做。
operator overloading(操作符重载-1) this
inline complex&
__doapl(complex* ths, const complex& r)
{
    ths->re += r.re;
    ths->im += r.im;
    return *ths;
}
inline complex&
complex::operator += (const complex& r)
{
    return __doapl (this, r);
}
{
    complex c1(2,1);
    complex c2(5);
    c2 += c1;
}+=操作符重载,函数出现了c2和c1,而函数参数列表只有r,函数体又出现了this,这是因为操作符重载的时候,参数默认会有一个this,只是不显示出来(重载操作符的时候也不能写出来,写出来就会报错,但是函数内可以用这个this)。
inline complex&
complex::operator += (this, const complex& r)
{
    return __doapl (this, r);
}+=操作符重载函数等价于上面那个,实际上参数是有一个this的(注意写的时候不能写出来,并且老师举的例子中,this位于第一个参数,实际上也可以是最后一个参数,不确定,但肯定有这个东西)。在这个例子中,this指的就是+=号左边的c2。
return by reference(语法分析)
inline complex&
complex::operator += (const complex& r)
{
    return __doapl(this,r);
}
c3 += c2 += c1;如果要使用c3 += c2 += c1的写法,那么函数不能返回value,只能返回reference,因为c2 += c1返回的结果又作为右值和c3参与运算,但是const complex& r(常引用),是可以传右值的,所以这一块我没理解。师兄写代码实测了一下,发现是可以返回value的。
输出操作符重载
#include <iostream.h>
ostream&
operator << (ostream& os, const complex& x)
{
    return os << '(' << real(x) << ',' << imag(x) << ')';
}
{
    complex c1(2,1);
    complex c2(2,-1);
    cout << c1;
    cout << c1 << c2;
}这里有几个知识点,首先是这个操作符只能定义为非成员函数; 再就是cout的类型是ostream,cout可以理解成一个屏幕,从做往右把东西往屏幕里丢(打印);所以正如前面所说,<<运算符是从左往右的,跟+=号那些不一样(那些是从右往左);考虑第二中cout的用法,即先输出c1再输出c2,因此重载函数返回类型不能是void,因为如果返回是void,那么cout<<c1运算完后没东西了,不能继续参与<<c2的运算了。
带指针的class
class String
{
public:
    String(const char* cstr = 0);
    String(const String& str);
    String& operator=(const string& str);
    ~String();
    char* get_c_str() const { return m_data;}
private:
    char* m_data;
}私有成员变量是一个指针,并不是一个数组,这是因为,带有指针的class里,通常用私有成员变量记录一个地址,这个地址存储的才是这个类的内容。如果私有成员变量不用指针,而是直接用数组存储一个字符串,由于字符串的长度是不确定的,那么在创建数组的时候也确定不了长度。
构造函数和析构函数
inline
String::String(const char* cstr = 0)
{
    if (cstr) {//如果cstr != 0
        m_data = new char[strlen(cstr)+1];
        strcpy(m_data, cstr);
    }
    else {
        m_data = new char[1];
        *m_data = '\0';
    }
}
inline
String::~String()
{
    delete[] m_data;
}字符串中,字符串数组的长度通常比字符串的实际长度多一位(比如一个字符串是"hello",那么该字符串的长度为5,但是数组长度为6,在结尾还有一个'\0')。有两种情况:一种是数组开头记录这个字符串的长度,然后接字符串;一种是数组前面记录字符串,在最后接'\0'。C和C++是后者。
带有指针的类必须要有拷贝构造和拷贝赋值两个函数
假设我们有两个String,分别为a和b。已知带指针的类,成员变量是一个指针,如果不重新写拷贝构造和拷贝赋值两个函数,用b构造a,或者将b赋值给a,此时编译器会自动调用默认的构造或赋值操作,那么a原本指向的地址就会丢失,造成内存泄漏,同时a和b指向同一个地址,对其中一个字符串做出更改,另外一个字符串也会受到影响,这种拷贝被称为浅拷贝。
拷贝构造函数
inline
String::String (const String& str)
{
    m_data = new char[strlen(str.m_data)+1];
    strcpy(m_data, str.m_data);
}
//网上查的strcpy源码
char* strcpy(char * dst, const char * src)
{
    char * cp = dst;
    while( *cp++ = *src++ )
        ;                            /* Copy src over dst */
    return( dst );
}
//网上查到的strlen源码
int strlen( const char* str )
{
    const char* ptr = str;
    while ( *str++ )
        ;
    return ( str - ptr - 1 );
}stack(栈)和heap(堆)
{
    Complex c1(1,2);//c1所占用的空间来自stack,这个作用域结束后,c1会自动消亡
    Complex* p = new Complex(3);//Complex(3)是个临时对象,其所占用的空间是以new自heap动态分配而得,并由p指向,因此在作用域结束前,需要手动释放这个对象
}stack是存在于某个作用域(比如一个函数、一个大括号括起来的部分)的一块内存空间,比如调用函数时,函数本身会形成一个stack用来放置它所接受的参数,以及返回地址。在函数本体内声明的任何变量,其所使用的内存块都取自上述stack。
heap是指由操作系统提供的一块global内存空间,程序可动态分配从中获得若干区块。
static local objects的生命周期
{
    static Complex c2(1,2);
}c2便是所谓的static object,其生命会在作用域结束后仍然存在,直到整个程序结束。注意,尽管c2生命周期没有结束,出了这个作用域之后,仍然无法直接访问。那这个特性可以在什么场合应用到呢,我做了一个简单的测试,仅代表一种使用方式。
int& fnc()
{
    static int t = 0;
    return t;
}
void main()
{
    int a = fnc();
    cout << a << "\n";
}之前提过,一个函数能不能返回引用,看它返回的东西是不是局部变量,如果是在函数里创建的对象,那么就不能传引用。但是如果在函数里创建的是static object,那么依然可以返回引用,因为这个对象知道整个程序结束才会消亡。
new:先分配memory,再调用ctor
Complex* pc = new Complex(1,2);
//new这个过程可以看作是以下三个步骤
{
    Complex* pc;
    void* mem = operator new( sizeof(Complex) );//operator new是C++自带的一个函数,这一步用来分配内存
    pc = static_cast<Complex*> (mem);//C++的强转符号
    pc->Complex::Complex(1,2);//调用构造函数,Complex是成员函数,所以参数里隐含了一个this,也就是pc
}delete:先调用dtor,再释放memory
String* ps = new String("Hello");
delete ps;
//可以看成是下面两步
{
    String::~String();//调用析构函数
    operator delete(ps);//释放内存空间,operator delete是C++自带的函数
}array new一定要搭配array delete
String* p = new String[3];
delete[] p;//当new了一个数组,delete加中括号,编译器就知道,要调用3次析构函数,把每个对象的成员指针释放
String* p = new String[3];
delete p;//如果不加中括号,编译器只会调用1次析构函数,这样只有第一个对象的成员指针被释放,造成内存泄漏注意,无论是上面还是下面的写法,最后p都会被释放掉, 因此造成内存泄漏的是对象里的成员指针。所以,如果对象是不带指针的类,那么上面和下面两种写法都不会造成内存泄漏,但是应该避免下面的写法。
关于&的不同含义
inline
String& String::operator= (const String& str)
{
    if(this == &str){
        return *this;
    }
    ...
}参数列表的String&表示的是传引用,if条件里的&str指的是取str对象的地址。
static
complex c1,c2,c3;//re,im等非静态成员变量会被创建三份
cout << c1.real();//等价于cout << complex::real(&c1),但是不能这么写
double real() const{
    return this->re;
}
//等价于下面这样,但是我们写的时候不能这么写
double real(this) const{
    return this->re;
}一个类的成员变量和成员函数可以写成静态或非静态的(静态就是在前面声明的时候加static),使用一个类创建多个对象时,非静态成员变量会被创建多份,而静态成员变量只会被创建一份(老师给的例子是银行账户,每个账户就是一个对象,每个对象的余额是不同的,需要分开存储,所以写成非静态成员变量,而利率是大家都相同的,所以写成静态成员变量,只需存储一份,所有的账户都相同)。非静态成员函数的参数会隐含一个this pointer,可以在函数里面使用,但是不能在参数列表体现,而静态成员函数没有this pointer,因此也不能处理非静态成员变量,只能处理静态成员变量。
class Account {
public:
    static double m_rate;
    static void set_rate (const double& x) { m_rate = x; }
};
double Account::m_rate = 8.0;//如果类里声明了静态变量,那么一定要在类后面加上这么一句,原因老师没讲
int main() {
    Account::set_rate(5.0);
    Account a;
    a.set_rate(7.0);
}静态成员函数有两种调用方法,一是可以不创建对象的情况下直接通过类名调用,二是创建一个对象,通过对象调用。
把构造函数放在private区
class A {
public:
    static A& getInstance() { return a; }
    setup() {...}
private:
    A();
    A(const A& rhs);
    static A a;
    ...
};
void main() {
    A::getInstance().setup();
}这是单例的一种写法,外界无法通过构造函数创建对象,因为构造函数放在了private区,只能通过getInstance()函数来获取事先创建好的对象a(因为getInstance()是静态函数,所以可以通过类名调用)。但这种写法有个问题,那就是无论使不使用这个类,对象a都已经创建了,会浪费空间。下面这种写法,当需要使用到A时,a才会被创建:
class A {
public:
    static A& getInstance();
    setup() {...}
private:
    A();
    A(const A& rhs);
    ...
};
A& A::getInstance() {
    static A a;
    return a;
}
void main() {
    A::getInstance().setup();
}注意,这里创建a的时候一定要是静态的,否则当getInstance函数结束的时候,a的生命也会消亡。
function template(函数模板)
stone r1(2,3), r2(3,3), r3;
r3 = min(r1, r2);
template <class T>
inline
const T& min(const T& a, const T& b) {
    return b < a ? b : a;
}//注意stone类必须重载小于号复合、委托和继承
复合可以理解成一个类A包含了另一个类B,其中A的函数可以用B来实现。而委托可以理解成一个类A包含了另一个类B的指针。复合和委托在构造的时候会有不同。
//Container是容器(类A),Component是组件(类B)
//构造由内而外
//Container的构造函数首先调用Component的构造函数,然后才执行自己
Container::Container(...) : Component() {...}
//可以不写Component(),这样编译器会自动调用默认构造函数,如果想调用其他构造函数,就要手动加上
//析构由外而内
//Container的析构函数首先执行Container的析构函数,然后才执行Component的析构函数
Container::~Container(...) { ... ~Component(); }从构造函数可以看出,用构造函数创建一个对象时,复合会先调用Component的构造函数创建另一个对象。而委托则是在要用到Component的时候,才会调用Component的构造函数进行创建。
class StringRep;
class String {
public:
    String();
    String(const char* s);
    ~String();
...
private:
    StringRep* rep; //pImpl
};
class StringRep {
friend class String;
    ...
};
这是一个委托的例子,相当于String这个class把功能都委托给StringRep这个class干,如果String的所有功能都用StringRep来实现,那么这种写法又称为pImpl。
class _List_node
    : public _List_node_base //public可替换成private、protected
{
    ...
};以上是继承的写法,注意,base class的构造函数不能是virtual,否则会出现undefined behavior
虚函数
class Shape {
public:
    virtual void draw() const = 0; //pure virtual
    virtual void error(const std::string& msg); //impure virtual
    int objectID()const; //non-virtual
    ...
};pure virtual函数:你希望derived class一定重新定义(override,覆写)它,它没有默认定义。
virtual函数:你希望derived class重新定义(override,覆写)它,它已有默认定义。
non-virtual:不希望derived class重新定义(override,覆写)它。
转换函数
class Fraction
{
public:
    Fraction(int num, int den=1)
        : m_numerator(num), m_denominator(den) { }
    operator double() const {
        return (double) (m_numerator / m_denominator);
    }
private:
    int m_numerator;
    int m_denominator;
};
void main() {
    Fraction f(3,5);
    double d = 4+f;
}转换函数和之前的操作符重载不一样的点在于:转换函数不需要返回类型,返回类型在函数名已经体现了。在使用时,编译器会先找有没有重载+操作符(int+Fraction),若没有,那么会把4和f都转换为double(此时f调用重载的double转换函数),进而变成两个double相加,得到结果d。
explicit
class Fraction
{
	public:
		Fraction(int num, int den = 1)
			: m_numerator(num), m_denominator(den) {}
		Fraction operator+(const Fraction& f) {
			return Fraction(...);
		}
	private:
		int m_numerator;
		int m_denominator;
};
void main()
{
	Fraction f(3,5);
	f + 4;
}前面提到,当Fraction和4相加时,由于重载了转换函数,所以会把f转换成double再与4相加,如果没有重载转换函数,但是重载了+操作符(接收Fraction),且单独一个4也能够调用Fraction构造函数构造实例,编译器会把4变成Fraction再调用+操作符进行运算。
class Fraction
{
	public:
		Fraction(int num, int den = 1)
			: m_numerator(num), m_denominator(den) {}
		operator double() const {
			return (double) (m_numerator / m_denominator);
		}
		Fraction operator+(const Fraction& f) {
			return *this;
		}
	private:
		int m_numerator;
		int m_denominator;
};
void main()
{
	Fraction f(3,5);
	f + 4; // [ERROR] ambiguous
}当既重载了转换函数又重载了+操作符,那么两种方法都可行,此时编译器就会报错。
class Fraction
{
	public:
		explicit Fraction(int num, int den = 1)
			: m_numerator(num), m_denominator(den) {}
		operator double() const {
			return (double) (m_numerator / m_denominator);
		}
		Fraction operator+(const Fraction& f) {
			return *this;
		}
	private:
		int m_numerator;
		int m_denominator;
};
void main()
{
	Fraction f(3,5);
	f + 4;
}如果在构造函数前加上explicit,就是告诉编译器,除非我明确要构造一个实例对象,否则你不要默认给我调用这个构造函数构造一个对象,那么此时只有一种情况,就是f通过转换函数变成double再参与运算。
pointer-like classes
一个像指针的类,即写出来的类像一个指针。作用是既想使用指针,又想用指针做更多东西,老师举了一个只能指针的例子。一个像指针的类需要重载两个操作符。
template<class T>
class shared_ptr
{
public:
    T& operator*() const
    { return *px; }
    T* operator->() const
    { return px; }
    shared_ptr(T* p) : px(p) { }
private:
    T* px;
    long* pn;
...
};使用方法如下:
struct Foo // 一个名为Foo的指针
{
    ...
    void method(void) { ... }
};
shared_ptr<Foo> sp(new Foo); // 指定shared_ptr的类型为Foo,创建一个名为sp的对象,而该类只有一个需要传指针的构造函数,所以传入new Foo(一个新建的Foo指针)
Foo f(*sp); // 调用sp的*操作符,返回sp的值,用来构建f
sp->method(); // 调用->操作符,按照常理来说,sp->等价于px,但是pxmethod()语法上是错误的,所以sp->method()等价于px->method()(编译器会这么解释),而px是Foo类型的指针,所以会调用Foo的methodfunction-like classes
仿函数,写出来的类像一个函数。老师暂时没有解释为什么要写一个类像指针或者像函数,这里仅讨论语法。一个类如果重载了操作符(),那么就可以说这个类是function-like classes。
template <class T>
struct identity {
    const T&
    operator() (const T& x) const { return x; }
};
template <class Pair>
struct select1st {
    const typename Pair::first_type&
    operator() (const Pair& x) const 
    { return x.first; }
};
template <class Pair>
struct select2nd {
    const typename Pair::second_type&
    operator() (const Pair& x) const
    { return x.second; }
};
template <class T1, class T2>
struct pair {
    T1 first;
    T2 second;
    pair() : first(T1()), second(T2()) { }
    pair(const T1& a, const T2& b)
        : first(a), second(b) { }
...
};值得一提的是,标准库中,仿函数一般都会继承奇特的base classes,比如unary_function、binary_function。
namespace
一个公司中,不同部门写的代码,难免会有重复的函数,但功能可能不一样,为了区别这些重复的函数,可以用namespace包起来,比如一个namespace名称为jj01,这样可以通过jj01::func1()来调用用jj01包起来的func1。
function template(函数模板)
函数模板在调用时不需要明确指明函数的类型:
template <class T>
inline
const T& min(const T& a, const T& b)
{
    return b < a ? b : a;
}
stone r1(2,3), r2(3,3), r3;
r3 = min(r1, r2);调用min时,会自动调用函数模板,该函数模板编译会被通过,但是不代表最终编译会通过,如果stone没有重载<,还是会报错。
member template(成员模板)
template <class T1, class T2>
struct pair {
    typedef T1 first_type;
    typedef T2 second_type;
    T1 first;
    T2 second;
    ...
    template <class U1, class U2>
    pair(const pair<U1, U2>& p)
        : first(p.first), second(p.second) {}
};类模板里面还能嵌套成员模板,其中T1和U1的关系是父子关系,子类可以初始化父类,而父类不能初始化子类。因为子类继承自父类,父类有的信息子类也都有,但是子类还有父类没有的信息,因此父类不能初始化子类。
template<typename _Tp>
class shared_ptr:public __shared_ptr<_Tp>
{
...
    template<typename _Tp1>
    explicit shared_ptr(_Tp1* __p)
        :__shared_ptr<_Tp>(__p){}
...
};这里的构造函数,__shared_ptr<_Tp>(__p)不能简单地理解成初始化成员变量(例如之前的复数的实部接小括号,小括号里面是初值),这里应该理解成用__p调用__shared_ptr<_Tp>(父类、基类)的构造函数,相当于用__p初始化基类。下面是一个实例:
#include <iostream>
// 基类模板,模拟 std::__shared_ptr
template<typename T>
class __shared_ptr {
protected:
    T* ptr;  // 管理的指针
public:
    // 构造函数
    explicit __shared_ptr(T* p = nullptr) : ptr(p) {
        std::cout << "__shared_ptr constructed with ptr = " << ptr << "\n";
    }
    // 析构函数
    ~__shared_ptr() {
        std::cout << "__shared_ptr destroyed with ptr = " << ptr << "\n";
        delete ptr;  // 释放资源
    }
    // 获取指针
    T* get() const { return ptr; }
};
// 派生类模板,模拟 std::shared_ptr
template<typename T>
class shared_ptr : public __shared_ptr<T> {
public:
    // 构造函数,接受任意类型的指针 _Tp1*
    template<typename _Tp1>
    explicit shared_ptr(_Tp1* p)
        : __shared_ptr<T>(p) {  // 调用基类构造函数
        std::cout << "shared_ptr constructed with ptr = " << p << "\n";
    }
    // 析构函数
    ~shared_ptr() {
        std::cout << "shared_ptr destroyed with ptr = " << this->get() << "\n";
    }
};
// 测试类
class MyClass {
public:
    MyClass() { std::cout << "MyClass created\n"; }
    ~MyClass() { std::cout << "MyClass destroyed\n"; }
    void greet() const { std::cout << "Hello from MyClass!\n"; }
};
int main() {
    {
        // 使用 shared_ptr 管理 MyClass 对象
        shared_ptr<MyClass> ptr(new MyClass());
        ptr.get()->greet();  // 调用 MyClass 的方法
    }  // ptr 离开作用域,对象被销毁
    return 0;
}
/*  
    输出:
    MyClass created
    __shared_ptr constructed with ptr = 0x1bdfe70
    shared_ptr constructed with ptr = 0x1bdfe70
    Hello from MyClass!
    shared_ptr destroyed with ptr = 0x1bdfe70
    __shared_ptr destroyed with ptr = 0x1bdfe70
    MyClass destroyed
*/specialization(模板特化)
和模板(泛化,类型T可以指定成任意类型)相对的是特化,特化可以指定当一个模板使用某种类型时,执行特定的动作。
template <class Key>
struct hash { }; //泛化,Key可以指定成int、long等类型
template<>
struct hash<char> {
    ...
};
template<>
struct hash<long> {
    ...
};在用hash创建一个对象时,如果使用long类型,那么可以用模板来创建这个对象,也可以用最下面的hash<long>来创建,这时候会优先使用后者来创建,如果使用模板,那么无论指定什么类型,除了类型不一样外,执行的动作应该都是一样的,但是可以用特化(比如hash<char>和hash<long>)来执行不同的动作。
partial specialization(模板偏特化--个数的偏)
当模板有多个待指定的类型时,可以特化其中的一个或多个类型
template<typename T, typename Alloc=...>
class vector
{
...
};
template<typename Alloc=...>
class vector<bool, Alloc>
{
...
};partial specialization(模板偏特化--范围的偏)
指针也是一种类型,写成下面这样,相当于把类型范围减小到了指针,也就是当T指定成指针时,会使用下面那个模板。
template <typename T>
class C
{
...
};
template <typename T>
class C<T*>
{
...
};
C<string> obj1;
C<string*> obj2;template template parameter(模板模板参数)
听起来很高级,其实就是模板的参数里又嵌套一个模板,下面是示例:
template<typename T, template<typename T> class Container>
class XCls
{
private:
      Container<T> c;
public:
      ...
};
XCls<string, list> mylst1; // 这种使用方法是错误的!!!这里的Container并不是指某个具体的类,只是一个代名词(就跟T不是某个具体的类型一个道理),在使用时第二个参数类型要传一个模板,然后使用这个传进去的模板和第一个参数类型T创建了一个c。注意上面代码中是使用方式是有误的,list是一个模板,但是要传两个类型,尽管第二个类型有默认类型,但是在这个模板模板参数(XCls)中,Container被声明为只接受单个参数,因此会报错,可以看下面的代码帮助理解:
template<typename T, typename Allocator = std::allocator<T>> class list;
template<typename T, template<typename T> class Container>
// list无法匹配Container的参数要求!!!但不代表XCls这个模板模板参数的写法是错误的,老师给出了正确的使用方法,但是没有过多解释:
template<typename T>
using Lst = list<T, allocateor<T>>;
XCls<string, Lst> mylst2;此外,其他正确的写法,但是这么写的话,传进去的模板必须是可以接受两个参数的模板:
template<typename T, 
         template<typename U, typename = std::allocator<U>> class Container>
class XCls {
private:
    Container<T> c;  // 使用默认的第二个参数(Allocator)
public:
    ...
};
XCls<string, list> mylst1;  // 正确:匹配两个模板参数(第二个用默认值)下面这种写法不是模板模板参数:
template <class T, class Sequence = deque<T>>
class stack {
    ...
protected:
    Sequence c;
};这只是一个普通的模板,第二个参数类型代名词为Sequence ,有默认值。
variadic template
翻译成数量不定的模板参数,指的是模板的参数个数是可变化的,看下面的例子:
void print() {
}
template <typename T, typename... Types>
void print(const T& firstArg, const Types&... args) {
    std::cout << firstArg << std::endl;
    print(args...);
}
int main() {
    print(7.5, "hello", std::bitset<16>(377), 42);
    
    return 0;
}
// 输出
7.5
hello
0000000101111001
42在这个例子中,print的参数理解成一个(firstArg)和一堆(args),使用时,可以同时传进多个参数,第一个参数就作为firstArg,其他作为args,然后递归调用print,每次从一堆中取出一个作为firstArg,由于最后会把args的参数都取完,所以还需要写一个空的print。这里只是variadic template的一个示例。此外,可以使用sizeof...(args)获取参数包的数量:
void print() {
}
template <typename T, typename... Types>
void print(const T& firstArg, const Types&... args) {
    std::cout << firstArg << std::endl;
    std::cout << "Remaining arguments: " << sizeof...(args) << std::endl;
    print(args...);
}
int main() {
    print(7.5, "hello", std::bitset<16>(377), 42);
    
    return 0;
}
// 输出
7.5
Remaining arguments: 3
hello
Remaining arguments: 2
0000000101111001
Remaining arguments: 1
42
Remaining arguments: 0ranged-base for
C++11遍历容器的新语法:
int main() {
    std::vector<double> vec;
    vec.push_back(1.0);
    vec.push_back(5.0);
    for(auto elem : vec) {
        std::cout << elem << std::endl;
        elem *= 3;
    }
    for(auto elem : vec) {
        std::cout << elem << std::endl;
    }
}
// 输出
1
5
1
5auto可以自动获取类型,比如这里的语法表示从vec依次取元素出来作为elem,那么auto就是double。从输出结果来看,从容器中取出元素这个过程是by value,因此在第一个循环中改变elem的值,实际上并没有影响到vec里的值。如果要改vec的值,就要传引用。
int main() {
    std::vector<double> vec;
    vec.push_back(1.0);
    vec.push_back(5.0);
    for(auto& elem : vec) {
        std::cout << elem << std::endl;
        elem *= 3;
    }
    for(auto elem : vec) {
        std::cout << elem << std::endl;
    }
}
// 输出
1
5
3
15reference
老师说,虽然引用的底层实现还是指针,但是引用可以理解成代表,引用和原变量的值、地址、大小都是一样的。引用不能重新代表其他变量。
#include <iostream>
using namespace std;
typedef struct Stag { int a, b, c, d; } S;
int main() {
    // 引用只能绑定一次
    int x1 = 0;
    int& r1 = x1;
    int x2 = 5;
    r1 = x2;
    cout << r1 << endl;
    cout << x1 << endl;
    // 引用的地址和内存、数值都是和原来一样的
    double x = 0;
    double* p = &x;
    double& r = x;
    cout << sizeof(x) << endl;
    cout << sizeof(p) << endl;
    cout << sizeof(r) << endl;
    cout << p << endl;
    cout << *p << endl;
    cout << x << endl;
    cout << r << endl;
    cout << &x << endl;
    cout << &r << endl;
    S s;
    S& rs = s;
    cout << sizeof(s) << endl;
    cout << sizeof(rs) << endl;
    cout << &s << endl;
    cout << &rs << endl;
    return 0;
}
// 输出
5
5
8
8
8
0x5ffe68
0
0
0
0x5ffe68
0x5ffe68
16
16
0x5ffe50
0x5ffe50一般来说,引用不能传右值,因此下面两个函数在传右值时可以同时存在,但是传左值就会报错:
void func(double& a) { cout << a << endl; }
void func(double a) { cout << a << endl; }
int main() {
    double a = 100;
    func(a); // 报错,ambiguity
    func(100); // 不报错,因为只有第二个函数适配
}但是有右值引用这个东西,这里不介绍用法:
void func(double&& a) { cout << a << endl; }
void func(double a) { cout << a << endl; }
int main() {
    func(100); // 报错,因为第一个函数也能接受右值,ambiguity
}继承+复合关系下的构造和析构
Derived的构造函数会先调用Base的默认构造函数,然后调用Component的默认构造函数,最后才执行自己的默认构造函数。析构函数的顺序则是反过来。
虚指针和虚函数表
当一个类有虚函数时,这个类除了存储成员变量外,还会存储一个虚指针,这个虚指针指向虚函数表,虚函数表里记录着虚函数的调用地址。子类也有自己的虚指针和虚函数表,但是虚函数表的内容会继承父类(如果子类重写了虚函数,那么虚函数表中该虚函数的调用地址会发生变化)。
关于this
this是一个类的函数都会隐含的参数,表示当前对象。
class CDocument {
    OnFileOpen() {
        ...
        Serialize(); // 相当于this->Serialize()
        ...
    }
    virtual Serialise();
};
class CMyDoc : public CDocument {
    virtual Serialize();
};
int main() {
    CMyDoc myDoc;
    ...
    myDoc.OnFileOpen();
    return 0;
}CMyDoc没有OnFileOpen函数,因此会调用父类的OnFileOpen函数,接着执行到Serialize时,由于调用的对象是myDoc(即this->Serialize的this是myDoc),因此会进入CMyDoc重写的Serialize中。
const
前面已经提到过,如果创建了一个常量(const)对象,该对象调用了一个非常量(无const)函数,编译器会报错。所以如果确定一个成员函数不会修改成员变量的值时,一定要加上const,变成常量函数。 以下是const的其他例子:
charT operator[] (size_type pos) const
{ ... }
reference operator[] (size_type pos)
{ ... }这是一个类下的两个成员函数,它们是可以同时存在的。当一个成员函数同时存在常量形式和非常量形式时,如果对象是常量,那么只会调用常量形式,而对象是非常量,那么只会调用非常量形式。
new,delete
new,delete是可以被重载的,既可以重载全局的,也可以重载一个类的(new一个对象时会调用)。
 
                   
                   
                   
                   
       
           
                 
                 
                 
                 
                 
                
               
                 
                 
                 
                 
                
               
                 
                 扫一扫
扫一扫
                     
              
             
                   550
					550
					
 被折叠的  条评论
		 为什么被折叠?
被折叠的  条评论
		 为什么被折叠?
		 
		  到【灌水乐园】发言
到【灌水乐园】发言                                
		 
		 
    
   
    
   
             
            


 
            