添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
C++基础提升及实战

C++基础提升及实战

1 头文件防卫式声明:

为了避免同一个头文件 line.h 在代码中被引用 #include 多次,C/C++ 中有两种方式宏实现头文件防卫式声明:

1.1 #ifndef 方式:

基本形式

宏定义名 _LINE_H 是可以自由命名的,但每个头文件的宏定义名都应该是唯一的,一般将宏定义名命名为头文件名的全大写形式,并以下划线为前缀,把文件名中的"."替换为下划线( line.h __LINE_H );

#ifndef 方式将头文件的内容包括在其中,具体形式如下:

#ifndef _LINE_H
#define _LINE_H
#include<iostream>
using namespace std;
class human
public:
    std::string GetName();
    int GetAge();
private:
    std::string mstrName;
    int miAge;
#endif

#ifndef 方式的优点:

  • #ifndef 属于 C/C++ 语言特性,可移植性较好,各个编译器都能支持;
  • #ifndef 不但可以针对整个文件内容,也可以针对文件中的具体一段代码;
//假设程序要暂时跳过一个代码块,但不从文件中删除该代码块
#include<iostream>
int main()
    #define _SKIP_
    #ifndef _SKIP_
    /*要跳过的代码*/
    std::cout << "这里被跳过" << std::endl;
    #endif
    std::cout << "这里被执行" << std::endl;
    return 0;
}
  • 既可以保证某一头文件不会被包含多次,又能保证内容完全相同的两个文件不会被同时包含;

#ifndef 方式的缺点:

  • 如果不同头文件中的宏定义名相同时,可能就会导致头文件明明存在,但编译器却硬说找不到声明的状况;
  • 由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,ifndef会使得编译时间相对较长;

1.2 #pragma once 方式:

基本形式:

在头文件的第一行加上 #pragma once

#pragma once
#include<iostream>
using namespace std;
class human
public:
    std::string GetName();
    int GetAge();
private:
    std::string mstrName;
    int miAge;
};

#pragma once 方式的优点:

  • 更高效,因为它不需要打开文件,就可以判断这个文件有没有被包含;
  • 因为没有使用宏定义名,所以不存在 #ifndef 方式的宏定义名冲突,也就不会引发相应的错误;

#pragma once 方式的缺点:

  • 属于编译器提供的指令,一些老的编译器不支持,可移植性较差;
  • 不能对一个头文件中的某一段代码作#pragma once声明,只能针对文件;
  • 只能保证同一个文件不会被引用多次,但是当引用了两个内容相同的不同名文件时,仍会出现重复引用错误;

2 默认构造函数:

默认构造函数是可以不用实参进行调用的构造函数,它包括了以下两种情况:

  • 没有带明显形参的构造函数。
  • 提供了默认实参的构造函数。

强调“没有带明显形参”的原因是,编译器总是会为构造函数形参表插入一个隐含的this指针,所以”本质上”是没有不带形参的构造函数的;

如果没有显式的写出任何构造函数,那么编译器将自动生成一个默认的无参构造函数,一旦你在类中定义了构造函数,那么编译器将不再生成默认的构造函数。


4 常对象与常成员函数:

4.1 常对象:

指对象的数据成员不能被修改;一旦将对象定义为常对象之后,就只能调用类的 const 成员(包括 const 成员变量和 const 成员函数);

  • 常对象调用常成员函数 可以
  • 常对象调用普通成员函数 不行,常量对象一旦初始化后,其值就再也不能更改;因此,不能通过常量对象调用普通成员函数,因为普通成员函数在执行过程中有可能修改对象的值;

4.2 常成员函数:

常成员函数:指由 const 修饰符修饰的成员函数,在常成员函数中不得修改类中的任何数据成员的值;

  • 普通对象调用常成员函数 可以
  • 常成员函数调用常成员函数 可以
  • 常成员函数调用非常成员函数 不行(静态成员函数除外,常成员函数可以调用静态成员函数)

两个成员函数的函数名和参数相同,但一个为 const ,一个不是,则它们构成重载;

普通函数(非类的成员函数)不能定义为 const ;

#include<ostream>
using namespace std;
class OBJ {
public:
    void Print() { }
int main()
    const OBJ obj;
    obj.Print(); //error, 不能通过常量对象调用普通成员函数
    return 0;
}

5 常指针与指向常量的指针:

根据const修饰的类型的不同来判断,const修饰p就是常指针,const修饰*p就是指针常量;

5.1 常指针:

int* const p

指针变量所指的空间地址是常量,不可以重新指向其他地址;

const修饰的时p, p是int* 类型的指针,即const修饰的时指针,即指针不能指向新的地址;

int* const p = new int;
*p = 10; //ok
p++; //error

5.2 指针常量:

const int* p;

int const *p;

指针指向的为常量,指针可以指向其他地址,但是该常量的值不能修改;

const修饰的是*p, 即指针指向指向地址的内容是常量;

const int* p = new int;
p++; //ok
*p = 23; //error

6 指针的空间分配:

Time *pt; 只是定义了指向 Time 类对象的指针变量 pt ,并没有实例化对象;

定义一个指向对象的指针有两种方法:

Time t;//实例化Time类对象t
Time *pt=&t;//定义pt为指向Time类对象t的对象指针

或者

Time *pt = new Time();

7 成员函数的隐藏参数-this指针:

在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址,调用成员函数时会将对象的地址作为实参传递给 this 指针, this 指针是所有成员函数的隐含参数,因此,在成员函数内部,它可以用来指向调用对象; 对于成员函数 Student::InitStudent(char * name,char *gender,int *age); ,如果显式的写出this指针,则为 Student::InitStudent(Student* const this,char * name,char *gender,int *age); ;

友元函数没有 this 指针,因为友元不是类的成员;只有成员函数才有 this 指针;

this const 指针,它的值是不能被修改的,一切企图修改该指针的操作,如赋值、递增、递减等都是不允许的;

this 指针只能在成员函数内部使用,用在其他地方没有意义,也是非法的;

只有当对象被创建后 this 才有意义,因此不能在 static 成员函数中使用;

8 初始化列表:

构造函数初始化列表:

构造函数():变量名1(数值),变量名2(数值){}

构造函数的初始化列表需要和数据成员声明的顺序保持一致,不然会有warning或者意想不到的错误;

类的常成员变量只能只用初始化列表初始化,而不能在构造函数函数体内进行赋值,如下:

#include<iostream>
using namespace std;
class human
public:
    human(string name, int age):mstrName(name),miAge(age)
    //warring
    //human(string name, int age):miAge(age),mstrName(name)
    //error
    //human(string name, const int age)
    //    miAge = age;
    //    mstrName = name;
    void info()
        cout << mstrName << " is " << miAge << " years old." << endl;
private:
    const string mstrName;
    int miAge;
int main()
    human xiaoming("xiaming",18);
    xiaoming.info();
    return 0;
}

C++ 11 列表初始化容器: C++11的新特性 initializer_list 用来做函数的参数,函数可以接收可变长度的传参;

#include<iostream>
using namespace std;
void print(initializer_list<int> list)
    for (auto it = list.begin(); it != list.end(); ++it)
        cout << *it << " ";
    cout << endl;
int main()
    print({ 1,2,3,4,5,6,7 });
    return 0;
}

9 友元函数与友元类:

9.1 友元:

朋友是值得信任的,所以可以对他们公开一些自己的隐私;

友元破坏了类的封装性,尽量少用

类A的友元B(友元函数、友元类)有权访问类A的所有私有(private)成员 和保护(protected)成员;

友元不属于 public private 或者 protected ,友元的声明可以出现在类的任何地方,包括在private和public部分;

友元不具有传递性:类 A 是类 B 的友元,类 B 是类 C 的友元,并不能导出类 A 是类 C 的友元;“咱俩是朋友,所以你的朋友就是我的朋友”这句话在 C++ 的友元关系上是不成立的,具体如下:

#include <iostream>
class A
private:
    int a;
public:
    friend class B;
class B
    friend class C;
public:
    void fun(A& ob)
        std::cout << ob.a << std::endl;//ok,可以访问
class C
public:
    void fun(A& ob)
        std::cout << ob.a << std::endl;//error,不能访问
    friend class B;
};

9.2 友元函数:

在类的定义中函数原型前使用关键字 friend ,可以把一些函数(包括全局函数和其他类的成员函数)声明为该类的友元函数,在友元函数内部就可以访问该类对象的所有私有 private 成员和保护 protected 成员;

尽管友元函数的声明有在类的定义中出现过,但是友元函数并不是类的成员函数;

9.3 友元类:

A 将类 B 声明为自己的友元,那么类 B 的所有成员函数就都可以访问类 A 对象所有私有成员;

友元访问私有成员的方式: 通过在友元函数的形参中定义对象来访问,具体如下:

#include <iostream>
class A
private:
    int a;
public:
    friend class B;
class B
    friend class C;
public:
    void fun(A& ob)//通过在形参中声明对象来访问;
        std::cout << ob.a << std::endl;//访问类A的私有成员
};

9.4 友元的继承问题:

  • 友元关系不能被继承
  • A 是类 B 的父类,类 A C 的友元,则类 B 不是类 C 的友元;
  • A 是类 B 的父类,类 C A 的友元,则类 C 是类 B 的友元;(此处有争议)
#include <iostream>   
class Father
    friend class Father2;
protected:
    int i;
class Son : public Father
protected:
    int j;
class Father2
public:
    int mem(Father obj) { return obj.i; } // ok
    int mem(Son obj) { return obj.i; } // ok,此处有争议
class Son2 : public Father2
public:
    int mem(Father obj) { return obj.i; } // error,友元关系不能被继承
};

10 拷贝构造函数与赋值运算符重载

赋值运算符重载是将已存在的对象赋值给新的对象;而拷贝构造函数会直接创建一个新的对象。

10.1 基本形式

拷贝构造函数重载:

赋值运算符重载:

  • 当没有自定义赋值运算符重载函数时,编译器会自动生成一个默认的赋值运算符重载函数;
  • 当没有自定义拷贝构造函数时,编译器会自动生成一个默认的拷贝构造函数;

10.2 调用时机:

#include<iostream>
using namespace std;
class human
public:
    human()
    human(int data):miData(data)
    ~human()
    human(const human& obj)
        miData = obj.miData;
        cout << "拷贝构造函数重载" << endl;
    human& operator= (const human& obj)
        miData = obj.miData;
        cout << "赋值运算符重载" << endl;
        return *this;
    void print()
        cout << miData << endl;
private:
    int miData;
int main()
    human obj1(1);
    human obj2(2); 
    obj2.print();
    //在对象obj2已经存在的情况下,用obj1来为obj2赋值,调用的是赋值运算符重载函数
    obj2 = obj1; 
    obj2.print();
    human obj3(3);
    //用obj3来初始化obj4,调用的是拷贝构造函数
    human obj4 = obj3;
    obj4.print();
    human obj5(5);
    //用obj5来初始化obj6,调用的是拷贝构造函数
    human obj6(obj5);
    obj6.print();
    return 0;
}

10.3 注意事项

保障连续赋值:

赋值运算符重载的返回值为 human& 类型,才能保证连续赋值;

#include<iostream>
using namespace std;
class human
public:
    human()
    human(int data):miData(data)
    human& operator= (const human& obj)
        miData = obj.miData;
        cout << "赋值运算符重载" << endl;
        return *this;
    void print()
        cout << miData << endl;
private:
    int miData;
int main()
    human obj1(1);
    human obj2(2);
    human obj3 = obj2 = obj1;
    obj3.print();
    return 0;
}

避免自我赋值引发的问题:

上述的赋值运算符重载函数仍有纰漏,自我赋值在某些情况下会导致内存重复释放的问题,具体待后续补充; 如下,通过添加if来避免自我赋值时带来的问题;

human& operator= (const human& obj)
        if(*this == obj)
            return *this;
        miData = obj.miData;
        cout << "赋值运算符重载" << endl;
        return *this;
    }

11 输入输出运算符重载:

11.1 需声明为友元函数:

输入输出运算符重载均要定义为 friend ,因为成员函数的第一个参数为隐藏的 this 指针,友元函数参数中无this指针,而输出输出运算符重载要求其第一个参数为输入输出流,所以定义为友元函数才能满足条件;

声明为友元函数时,重载函数要求的传参顺序为cin, obj,相当于operator>> (cin, obj),则使用时写为cin >> obj;

若不声明为友元函数,则重载函数要求的传参顺序为*this, cin,相当于operator>> (obj, cin),则使用时需写为obj >> cin;显然这并不符合我们的书写习惯,所以输入输出运算符重载均要定义为 friend

11.2 基本形式

C++ 中输入运算符的重载第一个参数是输入流对象的引用,第二个参数是需要输入的类对象的引用,返回值是输入流istream&;不同于输出运算符重载,对于输入运算符重载,将第二个参数定义为非const类型,否则无法赋值;

C++ 中输出运算符的重载第一个参数是输出流对象的引用,第二个参数是需要输出的类对象的引用,返回值是输入流ostream&;

ostream& operator<<(ostream& out,const A& a)
    out << miX << miY << endl;
    return out;