C++学习笔记

前言

学习C++之前建议要有下列基础:

  1. C语言
  2. 数据结构

C++是对C语言的扩充,C++多出来的语法都是为了弥补C语言的不足,只有熟悉C语言,并且在编写C语言代码中发现C语言的不方便,学C++的时候才有相见恨晚,大呼过瘾的感觉。

C++将常用的数据结构以标准库的形式提供给我们了,如果你有数据结构的基础,当你看到C++中的vector容器、string容器、list容器的时候,你就会惊呼,这不就是数据结构里面的内容么,所以这部分内容只需要大概扫一眼就行了,因为它们该有什么功能、能干什么、有什么特性,你都已经了然于胸。

此外你需要少许汇编语言的知识,因为理解static变量的时候需要用到数据段、代码段、栈段和堆的概念,不过这不是必须的,这部分知识现学也是可以的。

输出

#include <iostream>

int main(int argc, char const *argv[])
{
    std::cout << "Hello World!";
    return 0;
}

Hello World!

上面程序中std::cout的意思是使用std命名空间中的cout(console output)。

命名空间可以防止名字重复。也可以在程序头部使用using,这样在程序中再使用就不必每次都指定命名空间了。

#include <iostream>
using std::cout;

int main(int argc, char const *argv[])
{
    int a=123;
    float b =3.14;
    char c ='s';

    cout << a;
    cout << b;
    cout << c;
    return 0;
}

1233.14s

上面的程序没有换行,std::endl可以输出换行符,相当于\n,当我们需要引入多个std类里的静态成员时,可以直接设置using namespace std;:

#include <iostream>
using namespace std;

int main(int argc, char const *argv[])
{
    int a=123;
    float b =3.14;
    char c ='s';

    cout << a << endl;
    cout << b << endl;
    cout << c << endl;
    return 0;
}

123
3.14
s

引用

就是给变量起了另外一个名字

int ival = 1024;
int &refVal = ival;

inline限定符

函数调用是有开销的,小型的函数可以让编译器直接内联到使用的代码中,相当于把小型函数里的内容直接写到使用的地方。这样会省去函数调用的开销。

const限定符

定义常量,#define宏定义只是字符串替换,const是直接定义常量

const int bufSize = 512;

const修饰指针的时候比较容易弄混

int *const p1  // p1本身是常量,也叫常量指针
const int *p2  // p2是指向常量的指针
const int *const p3 // 常量指针指向常量

非常量指针不能指向常量

const double pi = 3.14;
double *ptr = &pi;     // 错误,ptr是非常量指针,不能指向常量
const double *cptr = &pi;  // 正确

vector容器

大小可变的数组

#include <iostream>
#include <vector>
using namespace std;

int main(int argc, char const *argv[])
{
    vector<int> ivec;
    ivec.push_back(1);
    ivec.push_back(2);
    ivec.push_back(4);
    ivec.push_back(6);

    for(int i=0;i<ivec.size();i++)
    {
        cout<<ivec[i]<<",";
    }
    return 0;
}

1,2,4,6,

string容器

相当于vector<char>,不过该容器多了对字符串的比较、替换等函数

#include <iostream>
#include <string>
using namespace std;

int main(int argc, char const *argv[])
{
    string s1 = "-----------";
    string s2 = "***********";

    // 截取字符串
    cout << s1.substr(0, 5) << endl;

    // 字符串拼接
    s1.insert(3, s2);
    cout  << s1 << endl;
    return 0;
}

-----
---***********--------

list容器

双向链表

#include <iostream>
#include <list>
using namespace std;

int main(int argc, char const *argv[])
{
    list<int> ilist;

    ilist.insert(ilist.begin(),1);
    ilist.insert(ilist.begin(),2);
    ilist.insert(ilist.begin(),4);
    ilist.insert(ilist.begin(),6);

    for (list<int>::iterator ite = ilist.begin(); ite != ilist.end(); ite++){
        cout << *ite <<endl;
    }
    return 0;
}

6
4
2
1

algorithm头文件

容器只提供了基础的功能,一些复杂的算法被放在algorithm头文件中。这些算法是通用的:可用于不同类型的容器和不同类型的元素。内容很多,包扩各种排序算法,二分查找算法等,读者可以自行查阅:

https://www.cplusplus.com/reference/algorithm/

类的定义

使用class和struct定义类唯一的区别就是默认的访问权限。

#include <iostream>

class Foo {
    
// 非成员函数想操作私有变量,可以通过友元实现。
friend void printa(const Foo&);
    
public:
    // 这里的const是修改隐式this指针的类型
    // 默认情况,this的类型Sale_data *const,加上const后变为const Sale_data *const
    // 这么做之后成员函数不能改变this的内容
    double getprice() const {
        return price;
    }
    
    // 成员函数可以在类内定义,也可以在类内声明,在类外定义
    double priceplus();
    
private:
    double price;
};
// 在类外部定义成员函数时要加上Foo::指明该函数的作用域
double Foo::priceplus()
{
    return ++price;
}
void printa(const Foo &item)
{
    std::cout << item.price << std::endl;
}

int main(int argc, char const *argv[])
{
    Foo F1;
    F1.priceplus();
    F1.priceplus();
    F1.priceplus();
    printa(F1);
    return 0;
}

3

类的创建

内存的分配方式有三种:

  1. 程序中的全局变量和static变量存在数据段中,也叫静态内存。
  2. 函数中定义的变量存在栈中。函数执行结束后,这些内存被释放。
  3. 剩余的地址空间被称作堆。可以用 malloc 或 new 申请任意多少的内存,不用时 free 或 delete 释放内存。

在堆中分配内存有不受栈大小的限制、多个线程可以共享数据等优点。缺点是在堆中分配的内存必须手动释放,否则会造成内存溢出。

对于一个简单的、运行时间很短、且堆空间足够用的程序而言,不会造成什么问题,在其进程结束时,操作系统会回收相关内存。但如果长期运行的服务器程序存在内存泄露,则会造成内存分配不成功, 由此引起致命错误。在引入请求分页机制的操作系统中,内存泄露实际上指的是进程的虚拟地址空间已经完全被分配完了。是的,物理内存很宝贵,但内存虚地址空间也是一种宝贵的资源。

class Foo{
    int a;
};

int main(int argc, char const *argv[])
{
    Foo F1; // 在栈中分配内存

    Foo *F2 = new Foo; // 在堆中分配内存
    delete F2; // 释放内存
    return 0;
}

类的初始化

#include <iostream>

class Foo {
    
public:
    // 构造函数,还有一种等价的写法
    // Foo(double p,int n):price(p),num(n){}
    Foo(double p,int n){
        price = p;
        num = n;
    }
    // 构造函数可以有多个,但只调用对应参数的那一个

    void print() const {
        std::cout << price << ","<< num << std::endl;
    }
    
private:
    double price;
    int num;
};

int main(int argc, char const *argv[])
{
    Foo F1(100,6);
    F1.print();
    return 0;
}

100,6

类的销毁

#include <iostream>

class Foo {
    
public:
    // 当类被销毁的时候会自动调用该函数
    ~Foo(){
        std::cout << "~Foo()"<< std::endl;
    }
};

int main(int argc, char const *argv[])
{
    Foo *F2 = new Foo; // 在堆中分配内存
    delete F2; // 释放内存
    return 0;
}

~Foo()

static静态变量

静态变量与全局变量都存在数据段中,他们只有作用域的区别:

  1. 全局变量在整个工程文件内都有效;
  2. 静态全局变量只在定义它的文件内有效;
  3. 静态局部变量只在定义它的函数内有效,因为静态局部变量不是存在栈中的,所以函数返回静态局部变量不会被释放。

同样的,静态函数也是一样的道理。

因为静态变量是存在数据段中的,静态函数是存在代码段中的,所以类中所定义的静态变量和静态函数无需实例化即可使用。但要注意,静态变量使用前必须初始化,这点和类中的其他成员不一样。

#include <iostream>  

class Foo  
{  
public:   
    Foo()  
    {    
        count++;  
    }  
    ~Foo()  
    {  
        count--;  
    }  
    static void output()  
    {  
        std::cout << count << std::endl;
    }  
private:  
    static int count;  
};

// 静态变量使用前必须初始化. 
int Foo::count = 666;  

int main()
{ 
    // 静态函数没实例化也可以直接调用,因为它是存储在代码段的
    Foo::output();

    Foo *p = new Foo; //
    p->output();
    delete p;

    Foo::output();
    return 0;
}

666
667
666

重载运算符

#include <iostream>
using namespace std;

class Foo{
public:
    int num;
    // 重载 + 运算符,用于把两个 Box 对象相加
    int operator+(const Foo& a)
    {
        return this->num+a.num;
    }
};

int main()
{ 
    Foo F1;
    F1.num = 1;
    Foo F2;
    F2.num = 2;
    cout << F1+F2;
    return 0;
}

3

#include <iostream>
using namespace std;

struct Array{
    Array(int length): m_length(length){
        m_p = new int[length];
        for(int i = 0; i < length; i++){
            m_p[i] = i * 5;
        }
    };
    ~Array(){
        delete[] m_p;
    };
    int & operator[](int i){
        return m_p[i];
    };
    int m_length;  //数组长度
    int *m_p;  //指向数组内存的指针
};

int main()
{ 
    Array A(10);
    for(int i = 0; i < A.m_length; i++){
        cout<<A[i]<<",";
    }
    cout<<endl;
    return 0;
}

0,5,10,15,20,25,30,35,40,45,

模板与泛型编程

#include <iostream>
using namespace std;

template<typename T> void Swap(T &a, T &b){
    T temp = a;
    a = b;
    b = temp;
}

int main(int argc, char const *argv[])
{
    int n1 = 100, n2 = 200;
    Swap(n1, n2);
    cout << n1 <<", " << n2 <<endl;

    float f1 = 12.5, f2 = 56.93;
    Swap(f1, f2);
    cout << f1 <<", "<< f2 << endl;


    char c1 = 'A', c2 = 'B';
    Swap(c1, c2);
    cout << c1<<", "<< c2 <<endl;
    return 0;
}

200, 100
56.93, 12.5
B, A

继承与渐增式开发

#include <iostream>
using namespace std;

class Father
{
public:
    void fun()
    {
        std::cout << "Father call function!" << std::endl;
    }
};

class Son : public Father
{
};

int main()
{ 
    Son * son = new Son;
    son->fun();
    return 0;
}

Father call function!

继承的优点之一是它支持渐增式开发,它允许我们在已存在的代码中引进新代码,而不会给原代码带来错误,即使产生了错误,这个错误也只与新代码有关。

虚函数

来看一段代码:

enum note {middleC,Csharp,Cflat};

class instrument{
public:
    void play(note) const {}
};

class wind : public instrument {};

void tune(instrument& i){
    i.play(middleC);
}

int main(){
    wind flute;
    tune(flute); 
    return 0;
}

tune函数的实参是instrument类型的,但是传入wind类型居然不出错。
仔细一想wind继承了instrument,说明wind也是instrument的一种呀,不算错也合理。这种将wind的对象、引用或指针转变成instrument对象、引用或指针的活动称为向上映射

向上映射让修改代码变得容易,例如我们对instrument不满意,于是继承下来做了一番修改,修改后的类叫做wind。如果没有向上映射功能的化,所有涉及到instrument的函数都要做修改,但是有了向上映射功能后,原本处理instrument的函数,仍然可以用来处理wind,无需修改。

但是向上映射功能引入了一个新的问题:

# include <iostream>
using namespace std;

enum note {middleC,Csharp,Cflat};

class instrument{
public:
    void play(note) const {
        cout << "instrument::play" <<endl;
    }
};

class wind : public instrument {
public:
    void play(note) const {
        cout << "wind::play" <<endl;
    }
};

void tune(instrument& i){
    i.play(middleC);
}

int main(){
    wind flute;
    tune(flute);
    return 0;
}

instrument::play

上面的函数我们传入的是wind类型的数据,但是由于进行了向上映射,被转换成了instrument类型,所以没有调用wind类中的函数,而是调用了instrument类中的函数。这显然与我们的意图不符。

为了解决这个问题,引入虚函数这一概念:

# include <iostream>
using namespace std;

enum note {middleC,Csharp,Cflat};

class instrument{
public:
    virtual void play(note) const {
        cout << "instrument::play" <<endl;
    }
};

class wind : public instrument {
public:
    void play(note) const {
        cout << "wind::play" <<endl;
    }
};

void tune(instrument& i){
    i.play(middleC);
}

int main(){
    wind flute;
    tune(flute);
    return 0;
}

wind::play

virtual实现play函数晚捆绑,即运行时确定对象的类型和合适的调用函数。同样调用play函数,因为参数不同,函数的处理逻辑完全不同,这就是所谓的多态

如果一个函数在基类中被声明为virtual,那么在所有的派生类中它都是virtual的。

我们可能会有这样一个疑问:“如果这个技术如此重要, 并且能使得任何时候都能调用“正确”的函数。那么为什么它是可选的呢?为什么不默认就是虚函数呢?”

答:因为它会影响效率。

抽象基类和纯虚函数

有的时候,我们想为某几个功能统一接口,他们的内部实现我们不管,但是给外部调用的接口要一致。

这时我们可以定义一个抽象基类,把统一的接口以纯虚函数的形式先定义好。所有的类都要继承这个抽象基类。

抽象基类本身是不可以实例化的,子类继承抽象基类后必须实现纯虚函数:

class base {
public:
    virtual void v() const = 0;
};

class d: public base {
};

int main(int argc, char const *argv[])
{
    base B;  // 错误,有纯虚函数的类不能实例化
    d D;     // 错误,子类继承有纯虚函数的类后,必须实现该函数
    return 0;
}

虚函数与构造函数、析构函数

构造函数不能是虚的。析构函数能够且常常必须是虚的。

虚析构函数会调用父类的析构函数,否则不会调用父类的析构函数。

# include <iostream>
using namespace std;

class base {
public:
    virtual ~base() {
        cout << "~base()" << endl;
    }
};


class derived: public base {
public:
    ~derived()  {
        cout << "~derived()" << endl;
    }
};


int main(int argc, char const *argv[])
{
    base * bp = new derived;
    delete bp;
    return 0;
}

结束语

以上就列举C++的主要特性,可以快速上手C++以形成生产力。可以看出C++相对C语言最主要的特征是多了类和泛型,这两个可是提升编码效率的大杀器。

C++还有很多语法,他们不太常用,而且不是C++的核心,所以用到的时候现查字典吧。

posted @ 2021-05-03 20:57:26
评论加载中...
发表评论