C++概述

C++详解

1 前期准备

1.1 历史发展

  • 1980 年,贝尔实验室的 Bjarne Stroustmp 开始对C语言进行改进,为其加入面向对象的特性。最初,这种新语言被称为“带类的C(C with Classes)”。
  • 1983 年,“带类的C”加入虚函数、函数和运算符重载、引用等概念后,正式定名为“C++(C plus plus)”。
  • 1985 年,C++ 最权威的著作、由 Bjarne Stroustmp 撰写的《C++程序设计语言》(The C+ + Programming Language)第一版发布
  • 1989 年,C++ 2.0 版发布,加入了多重继承、抽象类、静态成员、常量成员函数等概念。 *
  • 1990 年,又加入了模板、异常处理、名字空间等机制。
  • 1994 年,ANSI C++ 标准发布。
  • 1998 年,ANSI(美国国家标准协会)和 ISO(国际标准化组织)联合发布了至今使用最为广泛的 C++ 标准,称为 C++ 98。C++ 98 最为重大的改进就是加入了 “标准模板库”(Standard Template Library, STL),使得“泛型程序设计”成为 C++ 除“面向对象”外的另一主要特点。
  • 2003 年,ISO 的 C++ 标准委员会又对 C++ 略做了一些修订,发布了 C++ 03 标准。C++ 03 和 C++ 98 的区别对大多数程序员来说可以不必关心。
  • 2005 年,一份名为 Library Technical Report 1(简称 TR1)的技术报告发布,为 C++ 加入了正则表达式、哈希表等重要类模板。虽然 TR1 当时没有正式成为 C++ 标准,但如今的许多 C++ 编译器都已经支持 TR1 中的特性。
  • 2011 年 9 月,ISO 通过了新的 C+ + 标准,这就是 C++11。C++11 在酝酿的过程中被称为 C++ 0x,因为 Bjame Stroustmp 原本预计它应该在 2008 年或 2009 年发布。 C++11 对 C++ 的语言特性和标准库都做了比较大的扩充,TR1 中的许多特性正式成为 C++11 标准的一部分。

1.2 资料

1.2.1 书籍

  • 《C++ Primer》[第四版],作者 Stanley B.Lippman,Josée LaJoie,Barbara E.Moo
  • 《C++ Primer Plus》[第六版],作者 Stephen Prata
  • 《Effective C++》[第三版],作者 Scott Meyers
  • 《C++大学教程》[第九版],作者 Harvey M.Deitel Paul James Deitel
  • 《C++程序设计语言》[第一版],作者 Bjarne Stroustrup

1.2.2 网站

1.3 IDE

1.4 与C的区别

1.4.1 头文件

在 C++ 中,头文件不再以.h结尾,例如下一节提到的头文件 iostream。一些C语言中常用的头文件在 C++ 中的名字变为去掉.h,并在开头增加字符c。例如:

#include <cstdio>
#include <cstring>
#include <cstdlib>

1.4.2 强制类型转换

  • C: (int)3.5、(double)a
  • C++:int(3.5)、double(2)、double(a)
  • C++的强制类型转换看起来像函数调用,更加清晰

1.4.3 输入输出、命名空间

#include <iostream>

using namespace std;

int main()
{
    int a;
    char name[20];
    cin >> a >> name;
    cout << a << ' ' << name << endl;
    return 0;
}
命名命名空间语法格式
namespace namespace_name {
	//代码声明
}
调用命名空间中成员的语法格式
namespace_name::code; //code 可以是变量或函数

举例
namespace Ui
{
    class frmMain;
}
Ui::frmMain *ui;  

1.4.4 默认参数

1.4.4.1 默认参数只能放在函数声明和定义二者之一中
  • 在 C++ 中,声明一个函数时,可以为函数的参数指定默认值。当调用有默认参数值的函数时,可以不写出参数,这时就相当于以默认值作为参数调用该函数。
  • 默认参数可以放在函数声明或者定义中,但只能放在二者之一
  • 函数默认参数所带来的好处是使程序的可扩充性更好,即当程序需要增加新功能时,改动可以尽可能少
void Func(int x=20);  //函数的声明中,指明参数 x 的默认值是 20
Func();  //正确的调用语句,等效于 Function1(20);
int Max(int m, int n);
int a, b;
void Func(int x, int y=Max(a,b), int z=a*b){
    ...
}
Func(4);  //正确,等效于 Function2(4, Max(a,b), a*b);
Func(4, 9);  //正确,等效于 Function2(4,9 , a*b);
Func(4, 2, 3);  //正确
Func(4, ,3);  //错误!这样的写法不允许,省略的参数一定是最右边连续的几个
1.4.4.2 二义性
void fun(int one, int two = 2); 
void fun(int one);
  • 当这样调用时fun(1),上面两个函数都是完全匹配的.所以就有二义性,编译时会报错的。
  • 函数重载时谨慎使用默认参数值

1.4.5 引用

  • 某个变量的引用和这个变量是一回事,相当于该变量的一个别名
  • 值传递是不同的变量,改不了实参
  • 指针传递传的是地址,能通过地址改实参
  • 引用传递传的是别名,能改实参
  • C–值传递、指针传递和引用传递(C++)
  • 常引用,限定通过引用修改其引用内容[const int &b = a;],相对值传递和引用传递提高效率,不需要分配存储空间

1.4.6 inline内联函数

  • 函数:避免代码段的重写,减少程序体积,增加时间开销(形参和局部变量的空间分配、值传递、函数返回地址入栈等在函数调用时执行)
  • 内联函数(inline):调用时,直接将函数体的代码插入调用语句处。(当整体时间开销同形参和局部变量的空间分配、值传递、函数返回地址入栈等开销在一个量级上需要考虑)
  • 举例:采购开发板,每次采购需要知道型号、功能模块、资料等,分出去采购去做or开发去做
inline int Max (int a, int b)
{
    if(a >b)
        return a;
    return b;
}

1.4.7 函数的重载

1.4.7.1 重载示例与函数首地址打印
  • 函数重载(overload):函数名相同、参数表不同,不考虑返回值类型
#include <iostream>
#include <stdio.h>

using namespace std;

void Max(int a, int b)
{
    cout << "Max 1" << endl;
}
void Max(double a, double b)
{
    cout << "Max 2" << endl;
}
void Max(double a, double b, double c)
{
    cout << "Max 3" << endl;
}
int main()
{
    Max(3, 4);  //调用 void Max(int, int)
    Max(2.4, 6.0);  //调用 void Max(double,double)
    Max(1.2, 3.4, 5);  //调用 void Max(double, double, double)
    Max(1, 2, 3);  //调用 void Max(double, double, double)
    void (*ptr1)(int, int) = Max;
    printf("%p\n", ptr1); //打印int Max(int, int)函数的首地址
    void (*ptr2)(double, double) = Max;
    printf("%p\n", ptr2); //打印void Max(double,double)函数的首地址
    void (*ptr3)(double, double, double) = Max;
    printf("%p\n", ptr3); //打印void Max(double, double, double)函数的首地址
    //Max(3, 1.5);  //编译出错:二义性
    return 0;
}
1.4.7.2 二义性
  • 具体见上面代码段,能看到重载出现的二义性
  • 与默认参数的二义性有区别
1.4.7.3 重载特例-const常成员函数
#include <iostream>
using namespace std;

class A
{
public:
    void test()
    {
        cout << "void test()" << endl;
    }
    void test() const
    {
        cout << "void test() const" << endl;
    }
};

int main()
{
    A a1;
    a1.test();
    const A a2;
    a2.test();
}

1.4.8 动态存储机制(new\delete)

1.4.8.1 机制介绍
  • 变量需要分配内存,所以静态变量(如数组)需要预先定义好长度
  • 数组长度需求未知,避免空间浪费,使用“动态内存分配机制”
  • C代码对应的存储
  • 在C++中,内存区分为5个区,分别是堆、栈、自由存储区、全局/静态存储区、常量存储区
  • 堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。
  • 程序员可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了
1.4.8.2 new和delete VS malloc和free
  • new/delete是C++关键字,需要编译器支持,可以重载。
  • malloc/free是库函数,c中头文件是<stdlib.h>,C++中对应的是 cstdlib,不可以重载
  • 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸
#include <iostream>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int *ptr1 = (int *)malloc(2 * sizeof(int));
    printf("%p\n", ptr1);

    int *a = new int(10);
    printf("%p\n", &a);

    int *ptr2 = (int *)malloc(2 * sizeof(int));
    printf("%p\n", ptr2);

    int *b = new int(10);
    printf("%p\n", &b);

    return 0;
}
1.4.8.3 内存泄漏
  • 动态创建的对象用完后必须释放内存,避免造成内存泄漏,C++中用delete来完成
  • 普通的小程序,影响可以忽略。大程序,对内存要求很大的,内存泄漏后,内存的使用就会越来越多直到耗尽,然后程序挂掉。系统挂掉。。。
  • 服务器程序不能容忍内存泄漏的,特别是经常需要执行的代码的内存泄漏。因为服务器程序设计出来就是为了长期正常运行的,任何一点内存泄漏都会累积起来是服务器最后瘫痪。
1.4.8.4 野指针
1.4.8.5 示例
/* new & malloc */
int *pi = new int;	//该new表达式在自由存储区中分配创建了一个整形对象,
					//并返回一个指向该对象的地址来初始化指针pi
int *pi = new int();	//同上并初始化为0
int *pi = new int(1024);  //同上并初始化为1024

delete pi;	//释放pi指向的int型对象所占用的内存空间
p = NULL;	//pi已经无效,但是还在,置为0,清楚的表明指针不再指向任何对象,不然容易野指针

const int *pi = new const int(1024);	//动态创建的const对象必须进行初始化,
										//并且进行初始化后的值不能再改变

//数组动态内存分配
int *pi = new int[];               //指针pi所指向的数组未初始化
int *pi = new int[n];             //指针pi指向长度为n的数组,未初始化
int *pi = new int[]();            //指针pi所指向的地址初始化为0
delete [] pi;                   //回收pi所指向的数组

/* malloc & free */
void *malloc(size_t size);	//头文件为stdlib.h
void free(void *pointer);	//头文件为stdlib.h
//示例
int *p = (int *)malloc(10);	//指向整型的指针p指向一个大小为10字节的内存的地址
int *p = (int *)malloc(5 * sizeof(int)); //指向整型的指针p指向一个5个int整型空间的地址
//free释放
int *p=(int *)malloc(int);
if(pi == NULL)
    printf("Out of memory!\n");
free (p);
p = NULL;
  • 因为malloc()函数的返回值类型为void *,所以需要在函数前面进行相应的强制类型转换
  • 如果动态分配了一个数组,但是却用delete p的方式释放,没有用[],则编译时没有问题,运行时也一般不会发生错误,但实际上会导致动态分配的数组没有被完全释放。牢记,用 new 运算符动态分配的内存空间,一定要用 delete 运算符释放

1.4.9 string

1.4.9.1 string介绍&运算

用字符数组存放字符串容易发生数组越界的错误,而且往往难以察觉。因此,C++ 标准模板库设计了 string 数据类型,专门用于字符串处理。string 并不是 C++ 的基本数据类型,它是 C++ 标准模板库中的一个“类”

string 变量名;
string str1;  //定义 string 对象 str1,未初始化,值为空串“”
string city = "Suzhou";  //定义 string 对象 city,并初始化
/* 定义 string 对象数组 */
string as[] = {"Suzhou", "Lianyungang", "Taiyuan"};
cout << as[1];  //输出 Lianyungang
/* string对象的输入输出 */
string str1, str2;
cin >> str1 >> str2;
cout << str1 << " " << str2;
char name[] = "Lady Gaga";
string s1 = name;  //赋值后s1中的内容和name相同,修改s1不会影响name
/* 运算 */
string s1 = "123", s2 = "abc", s3;  //s3是空串
s3 = sl + s2;  //s3 变成"123abc"
s3 += "de";  //s3 变成"123abcde"
bool b = s1 < s3;  //b 为 true
char c = s1[2];  //c变成'3'(下标从0开始计算)
s1[2] = '5';  //s1 变成”125"
  • sizeof(string); //是固定的,不同编译器结果会有差别,
  • string 对象中只存放该内存空间的地址,或者再加上其他一些信息,字符串会在别处开辟内存空间存放
  • string 对象之间可以用 <、<=、==、>=、> 运算符进行比较,还可以用+将两个 string 对象相加、将一个字符串常量和 string 对象相加、将一个字符数组和 string 对象相加,相当于进行字符串连接。+=运算符也适用于 string 对象。此外,string 对象还可以通过[]运算符和下标存取字符串中的某个字符

1.5 编码规范

1.5.1 Google 与 Qt 编码风格

1.5.2 细节

在这里插入图片描述

1.6 思想

宋丹丹问赵本山:“把大象装进冰箱,需要几步?”
<C:面向过程>
第一步,打开冰箱
第二步,把大象塞进去
第三步,关上冰箱
<C++:面向对象>
第一步,把大象装冰箱实现为类的一个功能
第二步,调用该类,完成任务
在这里插入图片描述

2 语法

2.1 类和对象

2.1.1 类的定义

class 类名
{
访问范围说明符:
    成员变量1
    成员变量2
    成员函数声明1
    成员函数声明2

访问范围说明符:
    更多成员变量
    更多成员函数声明
    ...
};
“访问范围说明符”一共有三种,分别是 public、private 和 protected

类外实现成员函数
返回值类型 类名::函数名()
{
    函数体
}
#include <iostream>
using namespace std;

class Square
{
public:
    int area();
    void init(int l, int w);
protected:
    int length;
    int width;
};

void Square::init(int l, int w)
{
    length = l;
    width = w;
}

int Square::area()
{
    return (length * width);
}

int main()
{
    Square squ;
    squ.init(8, 8);
    cout << squ.area() << endl;
}

2.1.2 创建对象 & 创建对象指针

2.1.2.1 创建对象
  • 类名 对象名
    默认调用“对象名()”这个构造函数,在栈内存中存在对象名,在堆内存中存在实际对象;
  • 类名 对象名(一个或以上个参数)
    默认调用相应的构造函数,在栈内存中存在对象名,在堆内存中也是存在实际对象的;
  • 类名 对象名()
    不调用任何构造函数创建对象,仅在栈内在中存在对象名,在堆内存中并不存在实际的对象;
2.1.2.2 创建对象指针
2.1.2.3 对比

成员访问

. :对应实体
 >:对应指针

创建

  • 创建对象:
    隐式创建 & 显式创建
  • 创建对象指针:
    new创建放在自由存储区,需要手动delete,才能调用析构函数
    类似C中的malloc,其对应C中的堆,需要手动free,才能释放内存
    其他创建方式放在栈中
 对象指针:类名 *对象指针名;
2.1.2.4 示例代码
#include <iostream>
#include <stdio.h>
using namespace std;

class A
{
public:
    A()
    {
        cout << "A()" << endl;
        width = 1;
    }
    A(int a)
    {
        cout << "A(int a)" << endl;
    }
    ~A()
    {
        cout << "~A()" << endl;
    }
    void func();
    int width;
private:
    int length;
};

void A::func()
{
    cout << "void func();" << endl;
}

int main()
{
    A a1;
    printf("%p\n", &a1);
    A a2(100);
    printf("%p\n\n", &a2);
    A a3();
    //printf("%p\n", &a3); //没有分配地址运行出错

    //A a1 = A; //错误的
	A b2 = A(100);
	printf("%p\n", &b2);
	A b3 = A();
	printf("%p\n\n", &b3);

	cout << "****对象指针****" << endl;
	A *c1 = new A;
	printf("%p\n", &c1);
	A *c2 = new A();
	printf("%p\n", &c2);
	A *c3 = new A(100);
	printf("%p\n\n", &c3);

	cout << "****空间大小****" << endl;
	printf("%d\n", sizeof(A)); //打印出类所占空间大小,无成员变量,值是1
	printf("%d\n", sizeof(a1)); //打印出对象所占空间大小
	printf("%d\n\n", sizeof(c1)); //打印出对象指针所占空间大小(固定,同指针大小)

	cout << "****对象指针指向****" << endl;
	A *d1 = NULL; //声明了对象指针,没有初始化,与c1不同
    d1 = c1; //1、d1指向同一个对象
    printf("%p\n",&(d1->width));
    printf("%p\n",&(c1->width));
    d1 = new A; //初始化
	cout << "****访问****" << endl;
	a1.func();
	cout << a1.width <<endl;
	c1->func();
	(*c1).func();
	cout << c1->width <<endl;
	cout << "********" << endl << endl;

	delete c1;
	delete c1;
	delete c1;
	printf("\n");
}

2.1.3 静态成员变量和静态成员函数

  • 类的静态成员有两种:静态成员变量和静态成员函数。静态成员变量就是在声明时前面加了 static 关键字的成员变量;静态成员函数就是在声明时前面加了 static 关键字的成员函数。
  • 普通成员变量每个对象有各自的一份,而静态成员变量只有一份,被所有同类对象共享。
  • 访问普通成员时,要通过对象名.成员名等方式,指明要访问的成员变量是属于哪个对象的,或要调用的成员函数作用于哪个对象;访问静态成员时,则可以通过类名::成员名的方式访问,不需要指明被访问的成员属于哪个对象或作用于哪个对象。因此,甚至可以在还没有任何对象生成时就访问一个类的静态成员。当然,非静态成员的访问方式(也即对象名.成员名)其实也适用于静态成员,但效果和类名::成员名这种访问方式没有区别。
  • 与全局对象一样 对于静态数据成员 在程序中也只能提供一个定义 这意味着 静态
    数据成员的初始化不应该被放在头文件中 而应该放在含有类的非 inline 函数定义的文件中–《C++ Primer》中文第三版
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 使用 sizeof 运算符计算对象所占用的存储空间时,不会将静态成员变量计算在内。
#include <iostream>
using namespace std;

class CRectangle
{
private:
    int w, h;
    static int totalArea;  //矩形总面积
    static int totalNumber;  //矩形总数
public:
    CRectangle(int w_, int h_);
	CRectangle(CRectangle & r)
    ~CRectangle();
    static void PrintTotal();
};

CRectangle::CRectangle(CRectangle & r)
{
    totalNumber++;
    totalArea += r.w * r.h;
    w = r.w; h = r.h;
}

CRectangle::CRectangle(int w_, int h_)
{
    w = w_; h = h_;
    totalNumber++;  //有对象生成则增加总数
    totalArea += w * h;  //有对象生成则增加总面积
}

CRectangle::~CRectangle()
{
    totalNumber--;  //有对象消亡则减少总数
    totalArea -= w*h;  //有对象消亡则减少总而积
}

void CRectangle::PrintTotal()
{
    cout << totalNumber << "," << totalArea << endl;
}

int CRectangle::totalNumber = 0;
int CRectangle::totalArea = 0;
//必须在定义类的文件中对静态成员变量进行一次声明 //或初始化,否则编译能通过,链接不能通过

int main()
{
    CRectangle r1(3, 3), r2(2, 2);
    //cout << CRectangle::totalNumber; //错误,totalNumber 是私有
    CRectangle::PrintTotal();
    r1.PrintTotal();
    return 0;
}

2.1.4 常对象和常成员函数

  • 如果希望某个对象的值初始化以后就再也不被改变,则定义该对象时可以在前面加 const 关键字,使之成为常量对象(简称“常对象”)
  • 常量对象能调用常量成员函数
  • 见 1.4.7.3 重载特例-const常成员函数处示例代码

2.1.5 常对象和常成员函数

  • 一个类的成员变量如果是另一个类的对象,就称之为“成员对象”。包含成员对象的类叫封闭类(enclosed class)当封闭类的对象生成并初始化时,它包含的成员对象也需要被初始化,这就会引发成员对象构造函数的调用,可以通过在定义封闭类的构造函数时,添加初始化列表的方式解决。
  • 语法格式:
类名::构造函数名(参数表): 成员变量1(参数表), 成员变量2(参数表), ...
{
    ...
}
#include <iostream>
using namespace std;
class CTyre  //轮胎类
{
private:
    int radius;  //半径
    int width;  //宽度
public:
    CTyre(int r, int w) : radius(r), width(w) { }
};
class CEngine  //引擎类
{
};
class CCar {  //汽车类
private:
    int price;  //价格
    CTyre tyre;
    CEngine engine;
public:
    CCar(int p, int tr, int tw);
};
CCar::CCar(int p, int tr, int tw) : price(p), tyre(tr, tw)
{
};
int main()
{
    CCar car(20000, 17, 225);
    return 0;
}
  • 封闭类对象生成时,先执行所有成员对象的构造函数,然后才执行封闭类自己的构造函数
  • 当封闭类对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数,成员对象析构函数的执行次序和构造函数的执行次序相反,即先构造的后析构,这是 C++ 处理此类次序问题的一般规律。

2.1.6 C++的友元机制(友元函数和友元类)

  • 《c++ primer》:尽管友元被授予从外部访问类的私有部分的权限,但它并不与面向对象的编程思想相悖,相反,他们提高了公有接口的灵活性
  • 任何函数,或者成员函数或者类想成为某个类的友元,这是由这个类来决定的,而不能从外部强加友情,这尽力兼顾了类的封装性、数据的隐蔽性和灵活性。
  • 友元不是成员函数,但是它可以访问类中的私有成员。友元的作用在于提高程序的运行效率(即减少了类型检查和安全性检查等都需要的时间开销),但是,它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员
  • 【友元函数】在类内部,只能申明函数原型,不能定义函数体,不是访问级别的限制。
    【友元类】友类的每个成员都可以访问另一个类中的保护成员和私有成员。

2.1.7 this指针

  • this指针只能在一个类的成员函数中调用,它表示当前对象的地址
#include <iostream>
using namespace std;

class A
{
public:
    A(): ID(22), age(18)
    {
        cout << "A(): ID(22)" << endl;
        tmp = 66;
        this->tmp = 88;
        this->output();
    }
    void output()
    {
        cout << "void output()" << endl;
    }
    int tmp;
protected:
    int age;
private:
    int ID;
};

int main()
{
    A a1;
    cout << a1.tmp << endl;
    //cout << a1.age << endl;
    //cout << a1.ID << endl;
}

2.2 函数

2.2.1 内置函数 & inline

  • C++要求对一般的内置函数要用关键字inline声明,但对类内定义的成员函数,可以省略inline,因为这些成员函数已经被隐含地指定为内置函数了。 如果成员函数不在类体内定义,而在类体外定义,系统并不是把它默认为内置函数,调用这些成员函数的过程和调用一般函数的过程是相同的。如果想将这些成员函数指定为内置函数,则应该在定义时加inline关键字。from《C++ Primer》
  • inline函数必须跟申明放在同一个文件中,因为它是需要就地展开的。 如果你不把它放在.h中的话,那么在你调用这个函数时,只要包含这个文件能就编译成目标文件,但是在链接时,却会发现找不到这个函数体,而无法展开,从而造成链接失败。

2.2.2 构造函数 & 析构函数

C++中,构造函数与类名相同,析构函数前面加一个波浪线。析构函数,可以进行资源释放。都不需要写返回值。

  • 构造函数(constructor)用于对对象进行自动初始化
  • 构造函数可以被重载
  • 类的设计者没有写构造函数,编译器会自动生成一个无参的构造函数
  • 无参构造函数,不论是编译器自动生成的,还是程序员写的,都称为默认构造函数(default constructor),析构函数同
  • 创建对象过程中,先分配好内存,后初始化(即执行构造函数)
  • 拷贝有两种:深拷贝,浅拷贝
    当出现类的等号赋值时,会调用拷贝函数 在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。 但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。 所以,这时,必须采用深拷贝。 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。 简而言之,当数据成员中有指针时,必须要用深拷贝。

在这里插入图片描述
在这里插入图片描述

#include <iostream>
#include <string>
using namespace std;
/**
 * 定义类:Student
 * 数据成员:m_strName
 * 无参构造函数:Student()
 * 有参构造函数:Student(string _name)
 * 拷贝构造函数:Student(const Student& stu)
 * 析构函数:~Student()
 * 数据成员函数:setName(string _name)、getName()
 */
class Student
{
    public:
        Student() {
            m_strName = "jack";
            cout<<"Student()"<<endl;
        }
        Student(string _name) {
            m_strName = _name;
            cout<<"Student(string _name)"<<endl;
        }
        Student(const Student& stu) {
            cout<<"Student(const Student& stu)"<<endl;
        }
        ~Student() {
            cout<<"~Student()"<<endl;
        }
        void setName(string _name) {
            m_strName = _name;
        }
        string getName() {
            return m_strName;
        }
    private:
        string m_strName;
};

int main(void)
{
    Student stu2;
    cout << stu2.getName() << endl;
    // 通过new方式实例化对象*stu
    Student *stu1 = new Student("小王");
    cout <<stu1->getName()<< endl;
    // 更改对象的数据成员为“苏州”
    stu1->setName("苏州");
    // 打印对象的数据成员
    cout<<stu1->getName()<<endl;
    delete stu1;
    stu1 = NULL;
    return 0;
}
2.2.2.1 构造函数中的初始化列表
  • 初始化类的成员有两种方式,一是使用初始化列表,二是在构造函数体内进行赋值操作。
  • 初始化列表,一是省去调用默认构造函数的过程,对于数据密集型类来说,高效;二是有些成员变量的初始化必须放在初始化列表中(没有默认构造函数的类类型、需要初始化const修饰的类成员、需要初始化引用成员数据)。
  • 具体请看 2.6.2.1 构造函数
2.2.2.2 拷贝构造函数中 const 和 &
#include <iostream>
using namespace std;
class A
{
public:
    A(int tmp): numb(tmp) //带参数构造函数
    {
        cout<< "A(int tmp): numb(tmp)"<<endl;
    }
    A(const A &a) //拷贝构造函数
    {
        numb = a.numb;
        cout << "A(const A &a)"<<endl;
    }
    A& operator = (const A &a)//赋值函数(赋值运算符重载)
    {
        cout << "A& operator = (const A &a)" << endl;
        numb = a.numb;
        return *this;
    }
    void Func(A fa)
    {
    }
private:
    int numb;
};

int main()
{
    A a(2);
    A b(6);
    b = a;
    A c = a;
    b.Func(a);
    return 0;
}
返回值:
A a(2); // A(int tmp): numb(tmp)
创建对象a,并传参数2,调用带参的构造函数
A b(6); // A(int tmp): numb(tmp)
同上
b = a; // A& operator = (const A &a)
b和a都已经创建,此步只是将a赋值给b,调用赋值函数
A c = a; // A(const A &a)
创建c,同时将a赋值给c,调用拷贝构造函数
b.Func(a); // A(const A &a)
Func函数采用值传递方式,中间需要执行A fa = a; 的语句,需要调用拷贝构造函数
  • 加引用的原因
    执行b.Func(a);时,会调用拷贝构造函数,如果拷贝构造函数的参数不是引用,执行b.Func(a);时,会调用 A fa = a; 又因为之前没有创建又需要调用
    拷贝构造函数,故又执行 A fa = a; 这样永远递归调用下去,故拷贝构造函数是必须要带引用类型的参数的,而且也是编译器强制要求的。
  • 加const的原因
    避免通过引用改变实参的值

2.3 变量 & 对象

2.3.1 变量

(1)C中变量

  • 未初始化
    在这里插入图片描述
  • 初始化,由定义决定

(2)C++ 类中

  • 使用 sizeof 运算符计算对象所占用的存储空间时,不会将静态成员变量计算在内。

class A中如下直接声明赋值:

  • const static int ID = 1;
    C11标准可以编译
  • static int ID = 1;
    error: ISO C++ forbids in-class initialization of non-const static mumber ‘A::ID’
  • cons int ID = 1;
    waring: non-static data member initializers only available with -st=c++11
  • int ID = 1;
    waring: non-static data member initializers only available with -st=c++11
  • 注:有时使用方便,但是破会封装性,语法也有问题,慎用
    在这里插入图片描述

2.3.2 对象

  • 创建时,通过析构函数初始化
#include <iostream>
using namespace std;

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

int main()
{
    Test test;
    cout << "main()" << endl;
}

//打印结果是如下
//     Test()
//     ~Test()
//     main()

2.5 运算符重载

2.5.1 定义 & 语法

运算符重载:如果不做特殊处理,C++ 的 +、-、*、/ 等运算符只能用于对基本类型的常量或变量进行运算,不能用于对象之间的运算。有时希望对象之间也能用这些运算符进行运算,以达到使程序更简洁、易懂的目的。例如,复数是可以进行四则运算的,两个复数对象相加如果能直接用+运算符完成,不是很直观和简洁吗

  • 语法格式
返回值类型  operator  运算符(形参表)
{
    ....
}
  • 示例
#include <iostream>
using namespace std;
class Complex
{
public:
    double real, imag;
    Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) { }
    Complex operator - (const Complex & c);
};
Complex operator + (const Complex & a, const Complex & b)
{
    return Complex(a.real + b.real, a.imag + b.imag); //返回一个临时对象
}
Complex Complex::operator - (const Complex & c)
{
    return Complex(real - c.real, imag - c.imag);  //返回一个临时对象
}
int main()
{
    Complex a(4, 4), b(1, 1), c;
    c = a + b;  //等价于 c = operator + (a,b);
    cout << c.real << "," << c.imag << endl;
    cout << (a - b).real << "," << (a - b).imag << endl;  //a-b等价于a.operator - (b)
    return 0;
}

2.6 继承与派生

2.6.1 访问属性

两步走

  • 确定是否能继承
  • 能继承的成员,属性由继承属性和基类的成员属性决定(取权限最高的)
  • 类外能访问public成员,不能访问private成员,protected成员
  • 类内和友元可以访问private成员,protected成员,public成员
    在这里插入图片描述
#include <iostream>
using namespace std;

class A
{
public:
    int pub;
    void publ()
    {
        cout << "void publ()" << endl;
    }
    A()
    {
        pub = 1;
        pro = 2;
        pri = 3;
    }
    void getValue()
    {
        cout << this->pub << endl;
        cout << this->pro << endl;
        cout << this->pri << endl;
        this->publ();
        this->prot();
        this->priv();
    }
protected:
    int pro;
    void prot()
    {
        cout << "void prot()" << endl;
    }
private:
    int pri;
    void priv()
    {
        cout << "void priv()" << endl;
    }
};

class B: public A
{
public:
    void getValue1()
    {
        cout << this->pub << endl;
        cout << this->pro << endl;
        //cout << this->pri << endl; //私有成员不能继承
        this->publ();
        this->prot();
        //this->priv(); //私有成员不能继承
    }
};

class C: public B
{
public:
    void getValue2() // B public、protected 继承 A,运行没问题
    {
        cout << this->pub << endl;
        cout << this->pro << endl;
        //cout << this->pri << endl; //私有成员不能继承
        this->publ();
        this->prot();
        //this->priv(); //私有成员不能继承
    }
};

int main()
{
    cout << "****A****" << endl;
    A a;
    a.getValue();
    cout << a.pub << endl;
    a.publ();
    //cout << a.pro << endl; //类外不能访问protected成员
    //a.prot(); //类外不能访问protected成员
    //cout << a.pri << endl; //类外不能访问private成员
    //a.priv(); //类外不能访问private成员

    cout << "****B****" << endl;
    B b;
    b.getValue1();
    //cout << b.pub << endl; //public继承可以访问
    //b.publ(); //public继承可以访问

     cout << "****C****" << endl;
    C c;
    cout << c.pub << endl; // B: public A 可以访问,
}

2.6.2 构造函数 & 析构函数

2.6.2.1 构造函数
  • 对象的创建一定需要初始化
  • 派生类继承父类时会涉及成员变量的继承,创建子类的对象时,便涉及父类成员变量的初始化。
  • 父类没有声明构造函数
    (1)子类也没有声明自己的构造函数,则父类和子类均由编译器生成默认的构造函数
    (2)子类中声明了构造函数(无参或者带参),则子类的构造函数可以写成任何形式,不用顾忌父类的构造函数。在创建子类对象时,先调用父类默认的构造函数(编译器自动生成),再调用子类的构造函数。
  • 父类声明构造函数
    (1)父类只声明了无参构造函数
    如果子类的构造函数没有显式地调用父类的构造(即初始化列表没有调用),则将会调用父类的无参构造函数。也就是说,父类的无参构造函数将会被隐式地调用。
    (2)父类只声明了带参构造函数
    在这种情况下,要特别注意。因为父类只有带参的构造函数,所以如果子类中的构造函数没有显示地调用父类的带参构造函数,则会报错,所以必需显示地调用。关于构造函数的显示调用。
#include <iostream>
using namespace std;

class Person
{
public:
    Person(int id, int ag)
    {
        ID = id;
        age = ag;
    }
    int getBaseInfo()
    {
        cout << ID << endl;
        cout << age << endl;
    }
private:
    int ID;
    int age;
};

class Stu: public Person
{
public:
    Stu(int gra, int id, int ag): Person(id, ag)
    {
        grade = gra;
        cout << "Stu(int gra, int id, int ag): Person(id, ag)" << endl;
    }
    Stu(int gra): Person(320721, 18)
    {
        grade = gra;
        cout << "Stu(int gra): Person(320721, 18)" << endl;
    }
    int getStuInfo()
    {
        cout << grade << endl;
    }
private:
    int grade;
};

int main()
{
    Stu stu1(6, 320721, 18);
    stu1.getBaseInfo();
    stu1.getStuInfo();
    cout << "**********"<< endl;
    Stu stu2(6);
    stu2.getBaseInfo();
    stu2.getStuInfo();
}

2.7 多态与虚函数

2.7.1 虚函数

  • 所谓“虚函数”,就是在声明时前面加了 virtual 关键字的成员函数。

2.7.2 多态

  • 在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数
  • 存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
  • 抽象类是指包括至少一个纯虚函数的类
  • 多态用虚函数来实现,结合动态绑定
2.7.2.1 示例代码
#include <iostream>
#include <stdlib.h>
using namespace std;

class Father
{
public:
    void Face()
    {
        cout << "Father's face" << endl;
    }

    virtual void Say()
    {
        cout << "Father say hello" << endl;
    }
};


class Son: public Father
{
public:
    void Say()
    {
        cout << "Son say hello" << endl;
    }
};

int main()
{
    Son son;
    Father *pFather = &son; // 隐式类型转换
    pFather->Say();
}

在这里插入图片描述
编译步骤:

  • 编译器在编译的时候,发现Father类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表(即 vtable),该表是一个一维数组,在这个数组中存放每个虚函数的地址
  • 编译器另外还为每个对象提供了一个虚表指针(即vptr),这个指针指向了对象所属类的虚表,在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向了所属类的虚表,从而在调用虚函数的时候,能够找到正确的函数
  • 由于pFather实际指向的对象类型是Son,因此vptr指向的Son类的vtable,当调用pFather->Son()时,根据虚表中的函数地址找到的就是Son类的Say()函数
2.7.2.2 虚表机制
  • 具体运行机制见 2.7.2.1 示例代码
  • 核心是,由对象类型调用函数,类比 根据外设调用USB功能

2.8 三大特性五大原则

在这里插入图片描述

2.8.1 四大特性

2.8.1.1 抽象
  • 如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类 。
  • 基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重载以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。
  • 包含纯虚函数的类称为抽象类。
  • 抽象层函数依赖调用,涉及到子类继承实现,体现出5大原则中的依赖倒置原则。
2.8.1.2 封装
  • 封装就是隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读和修改的访问级别,将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成“类”,其中数据和函数都是类的成员。
  • 封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是要通过外部接口,以特定的访问权限来使用类的成员。
  • 将同一类的进行抽象,然后封装,如下图都有两只耳朵、四只腿、一双眼睛等属性,可封装为动物一类
    在这里插入图片描述
2.8.1.3 继承
  • 继承是面向对象的基本特征之一,继承机制允许创建分等级层次的类。继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
    在这里插入图片描述
2.8.1.4 多态
  • 多态同一个行为具有多个不同表现形式或形态的能力。是指一个类实例(对象)的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
    在这里插入图片描述

  • 多态的优点:
    1、消除类型之间的耦合关系
    2、可替换性
    3、可扩充性
    4、接口性
    5、灵活性
    6、简化性

  • 多态存在的三个必要条件:
    1、继承
    2、重写(子类继承父类后对父类方法进行重新定义)
    3、父类引用指向子类对象

  • 简言之,多态其实是在继承的基础上的。比如说今天我们要去动物园参观动物,那么你说我们去参观兔子、参观绵羊、参观狮子、参观豹子都是对的,但你不能说我们去参观汽车。在这个例子中,子类具有多态性:除了使用自己的身份,还能充当父类。

2.8.2 五大原则

2.8.2.1单一职责原则SRP(Single Responsibility Principle)

是指一个类的功能要单一,不能包罗万象。如同一个人一样,分配的工作不能太多,否则一天到晚虽然忙忙碌碌的,但效率却高不起来。

2.8.2.2 开放封闭原则OCP(Open-Close Principle)

一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。比如:一个网络模块,原来只服务端功能,而现在要加入客户端功能,那么应当在不用修改服务端功能代码的前提下,就能够增加客户端功能的实现代码,这要求在设计之初,就应当将服务端和客户端分开,公共部分抽象出来。

2.8.2.3 里式替换原则LSP(the Liskov Substitution Principle LSP)

子类应当可以替换父类并出现在父类能够出现的任何地方。比如:公司搞年度晚会,所有员工可以参加抽奖,那么不管是老员工还是新员工,也不管是总部员工还是外派员工,都应当可以参加抽奖,否则这公司就不和谐了。

2.8.2.4 依赖倒置原则DIP(the Dependency Inversion Principle DIP)

具体依赖抽象,上层依赖下层。假设B是较A低的模块,但B需要使用到A的功能,这个时候,B不应当直接使用A中的具体类: 而应当由B定义一抽象接口,并由A来实现这个抽象接口,B只使用这个抽象接口:这样就达到了依赖倒置的目的,B也解除了对A的依赖,反过来是A依赖于B定义的抽象接口。通过上层模块难以避免依赖下层模块,假如B也直接依赖A的实现,那么就可能 造成循环依赖。一个常见的问题就是编译A模块时需要直接包含到B模块的cpp文件,而编译B时同样要直接包含到A的cpp文件。

2.8.2.5 接口分离原则ISP(the Interface Segregation Principle ISP)

模块间要通过抽象接口隔离开,而不是通过具体的类强耦合起来

2.9 模板

  • 模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
  • 模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。
  • 每个容器都有一个单一的定义,比如 向量,我们可以定义许多不同类型的向量,比如 vector 或 vector 。
  • 可以用模板定义函数和类

2.9.1 函数模板

  • 语法格式如下
template <class type> ret-type func-name(parameter list)
{
   // 函数的主体
}
#include <iostream>
#include <string>
using namespace std;

template <typename T> inline T const& Max (T const& a, T const& b)
{
    return a < b ? b : a;
}

//template <typename T> T Max (T a, T b)
//{
//    return a < b ? b : a;
//}

int main ()
{

    int i = 39;
    int j = 20;
    cout << "Max(i, j): " << Max(i, j) << endl;

    double f1 = 13.5;
    double f2 = 20.7;
    cout << "Max(f1, f2): " << Max(f1, f2) << endl;

    string s1 = "Hello";
    string s2 = "World";
    cout << "Max(s1, s2): " << Max(s1, s2) << endl;

   return 0;
}

2.9.2 类模板

  • 语法格式如下
template <class type> class class-name {
.
.
}
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>

using namespace std;

template <class T>
class Stack {
  private:
    vector<T> elems;     // 元素

  public:
    void push(T const&);  // 入栈
    void pop();               // 出栈
    T top() const;            // 返回栈顶元素
    bool empty() const{       // 如果为空则返回真。
        return elems.empty();
    }
};

template <class T>
void Stack<T>::push (T const& elem)
{
    // 追加传入元素的副本
    elems.push_back(elem);
}

template <class T>
void Stack<T>::pop ()
{
    if (elems.empty()) {
        throw out_of_range("Stack<>::pop(): empty stack");
    }
    // 删除最后一个元素
    elems.pop_back();
}

template <class T>
T Stack<T>::top () const
{
    if (elems.empty()) {
        throw out_of_range("Stack<>::top(): empty stack");
    }
    // 返回最后一个元素的副本
    return elems.back();
}

int main()
{
    try {
        Stack<int>         intStack;  // int 类型的栈
        Stack<string> stringStack;    // string 类型的栈

        // 操作 int 类型的栈
        intStack.push(7);
        cout << intStack.top() <<endl;

        // 操作 string 类型的栈
        stringStack.push("hello");
        cout << stringStack.top() << std::endl;
        stringStack.pop();
        stringStack.pop();
    }
    catch (exception const& ex) {
        cerr << "Exception: " << ex.what() <<endl;
        return -1;
    }
}

2.10 STL(标准模板库)

2.10.1 介绍

  • C++ STL(标准模板库)是一套功能强大的 C++ 模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构,如向量、链表、队列、栈。

2.10.2 C++ 标准模板库的核心包括以下三个组件

  • 容器(Containers): 容器是用来管理某一类对象的集合。C++ 提供了各种不同类型的容器,比如 deque、list、vector、map 等。
  • 算法(Algorithms): 算法作用于容器。它们提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索和转换等操作。
  • 迭代器(iterators): 迭代器用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集。

3 知识点补充

3.1 下划线 ‘_’

3.1.1 形成风格

  • 命名规则
#ifndef IROBOT_H
#define IROBOT_H
......
void on_pb_control_clicked();       /* 控制界面触发 */
......
#endif // IROBOT_H
Class a
{
private:
	int _ID;	//私有成员,加_区别明显,具体各自公司代码规范或者是自己的编码风格
};

3.2 C++中关于class B:A与Class B::A问题

  • class B: A 等价于 class B: private A
class <派生类名>:<继承方式><基类名>
{
<派生类新定义成员>
};
  • Class A::B为类的嵌套关系,即A类是B类内部的类,双冒号为作用域

3.3 多态 & 重载

  • 多态:“动态绑定”
    是指子类重新定义父类的虚方法(virtual,abstract)。当子类重新定义了父类的虚方法后,父类根据赋给它的不同的子类,动态调用属于子类的该方法,这样的方法调用在编译期间是无法确定的。

  • 重载:“静态绑定”
    是指允许存在多个同名方法,而这些方法的参数不同。重载的实现是:编译器根据方法不同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。

4 示例代码

4.1 课程管理

4.1.1 介绍

课程管理系统设计(windows系统)

4.1.2 代码工程

下载链接

参考

1、C++入门教程,C++基础教程
2、cppreference
3、cplusplus
4、C++默认参数与函数重载 注意事项
5、面向对象的三大基本特征,五大基本原则
6、面向对象三大特性五大原则 + 低耦合高内聚
7、C++ 三大特性 封装,继承,多态
8、面向对象五大原则
9、浅谈new/delete和malloc/free的用法与区别
10、《C++程序设计》[第四版],作者谭浩强
11、《C++大学教程》[第九版],作者 Harvey M.Deitel Paul James Deitel
12、C++ 自由存储区是否等价于堆?
13、野指针及c++指针使用注意点
14、C++中的string详解
15、面向对象与面向过程的本质的区别
16、浅谈面向对象的编程思想:如何优雅地把大象装进冰箱?
17、C++类的成员函数(在类外定义成员函数、inline成员函数)
18、关于C++类的inline 成员函数
19、C++ 的浅拷贝和深拷贝(结构体)
20、C++面试题之浅拷贝和深拷贝的区别
21、c++深拷贝和浅拷贝
22、深入理解C++中public、protected及private用法
23、你必须知道的指针基础-8.栈空间与堆空间
24、C++继承中关于子类构造函数的写法
25、C++的初始化列表(Initilization List)
26、C++普通变量、C++静态成员变量、C++成员常量、C++静态成员常量的初始化方法
27、C++之友元机制(友元函数和友元类)
28、为什么复制构造函数的参数需要加const和引用
29、课程管理系统设计(windows系统)
30、关于C++类的静态数据为什么一定要初始化
31、c++中的抽象概念详解

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

worthsen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值