C语言学完之后赶紧来学C++,学完之后打算自己来做一个桌面应用
一、原理和概念的介绍
C++是如何运行的?
所有的.cpp文件都会被编译,而头文件不会。头文件通过预处理器(preprocessor)include(内容复制)到.cpp文件中。 每个.cpp文件都会被单独编译成一个.obj(二进制)文件。linker(链接器)负责将每个.obj文件联系起来,组成一个exe。
二、对比C语言后新的收获
头文件
声明和定义
在头文件中,只允许出现声明
其中:声明包括以下几种:
- 外部变量 extern修饰的变量
- 函数原型
- class/struct的声明
如果你在头文件中定义了一个变量,且在多个.cpp文件中又引入了这个头文件。则linker会报重复定义的错误。
头文件保护符
#include引号和方括号的区别
- 引号通常表示引入一个相对位置下的文件
- 方括号通常表示引入一个标准库
C语言的标准库通常带.h拓展名,而C++的标准库一般不带
#pragma once
试想以下情况: 当我们重复引入同一个头文件会发生什么情况?试看以下代码
// log.h
void loging(char *str);
struct Player
{
int age;
};
// header.c
#include <stdio.h>
#include "log.h"
#include "log.h"
int main(void)
{
char *a = "a";
loging(a);
}
void loging(char *str)
{
printf("%s", str);
}
没错,你会看到一个重复定义的报错。如何解决这个问题呢? 我们可使用 #pragma once 指令,告诉编译器对于这个头文件只处理一次
#pragma once
void loging(char *str);
struct Player
{
int age;
};
当然你可以使用条件编译
#ifndef LOG_H
#define LOG_H
void loging(char *str);
struct Player
{
int age;
};
#endif
流程控制语句(continue,break,return)
在循环中有以下的用法:
- continue 跳出本轮循环
- break 跳出本次所有循环
- return 跳出本次所有循环,且其后的所有代码都不会执行
指针
- 指针是一个整数,一个数字,它存储一个内存地址
- void指针,意味着我们不关心这个数据是什么类型,我们只想存放一个内存地址
内联函数
概念
以inline修饰的函数叫做内联函数, 编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。 (看到在加粗部分时,小伙伴肯定会想,这和c语言中的宏是不是很像了?)
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用
特性
-
inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长/递归的函数不适宜使用作为内联函数。
-
inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内代码比较长/递归等等,编译器优化时会忽略掉内联。
-
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到
三、refence引用和指针的对比
引用:
可以在某个变量类型后面加上个&,表示对某个变量的引用,相当于typedef。
#include <iostream>
int main(void)
{
int a = 5;
int &ref = a;
ref = 6;
std::cout << ref << std::endl; // 6
}
引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
- 引用在定义时必须初始化,指针没有要求。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
- 没有NULL引用,但有NULL指针。
- 在sizeof中的含义不同:引用的结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
- 引用进行自增操作就相当于实体增加1,而指针进行自增操作是指针向后偏移一个类型的大小。
- 有多级指针,但是没有多级引用。
- 访问实体的方式不同,指针需要显示解引用(&),而引用是编译器自己处理。引用比指针使用起来相对更安全。
四、class 类
4.1如何书写一个class
- 一个class通常由头文件.h声明和源文件.cpp分开书写。
- class的声明和原型都要放到头文件中。
- 函数的实体部分放到源文件.cpp中。
4.2成员变量
4.2.1.本地变量
在函数内部定义的变量,其作用域只在函数内有效
4.2.2.成员变量、参数、本地变量
成员变量
- 定义在构造函数和方法之外
- 其生存周期和实例对象一样
- 作用域和class作用域一样,所以在构造函数和成员方法内部都可以访问这个变量
- 在class内部声明的成员变量,其实是个外部变量
#include <iostream>
class A
{
private:
int i; // 相当于 extern int i ;
public:
void f();
};
void A::f()
{
int j = 10;
i = 11;
}
int main(void)
{
A a;
a.f();
}
4.2.3.成员变量和成员方法
成员方法是属于类的,而成员变量是属于每个实例对象的。
如上,用一个类生成多个实例,并调用成员方法改变实例里的成员变量。 成员方法是如何知道具体是哪个实例呢?
class A
{
private:
int i;
public:
void f();
};
void A::f()
{
i = 11;
std::cout << i << std::endl;
}
int main(void)
{
A a;
A a1;
a.f();
a1.f(); // 调用类的方法,如何修改实例?
}
在class中调用成员方法的时候会传入一个 隐藏的参数this 。this指向当前实例
4.3构造和析构
构造函数
- 构造函数会在实例化的时候自动调用
- 构造函数名必须与类名相同
- 构造函数没有任何返回值和返回类型
- 构造函数可以带参数,且可以有默认值
#include <iostream>
class Person
{
public:
Person(int defaultAge = 10, int defaultHeight = 170);
void show();
~Person();
private:
int age;
int height;
};
Person::Person(int defaultAge, int defaultHeight)
{
std::cout << defaultAge << " before construct " << defaultHeight << std::endl;
age = defaultAge;
height = defaultHeight;
}
Person::~Person()
{
std::cout << " before construct age = " << age << " ; height = " << height << std::endl;
}
void Person::show()
{
std::cout << "age=" << age << std::endl;
std::cout << "height=" << height << std::endl;
}
int main(void)
{
Person p1;
Person p2(12);
Person p3(21, 178);
p1.show();
p2.show();
p3.show();
}
析构
析构是一种特殊的成员函数,当对象的生命周期结束时,用来释放分配给对象的内存空间,并做一些清理的工作
- 析构函数名与类名必须相同。
- 析构函数名前面必须加一个波浪号(tide) ~ 。
- 没有参数,没有返回值,不能重载。
- 一个类中只能有一个析构函数。
- 没有定义析构函数,编译系统会自动为和这个类生成一个默认的析构函数。
4.4动态内存分配 new & delete
相当于C语言中的malloc和free
new到底干了什么?
- 分配内存空间
- 调用构造函数
delete到底干了什么?
- 调用析构函数
- 收回内存空间
4.5类的访问限制
主要有以下三个关键字来控制class的成员属性和方法的访问权限
- public 任何人都可以访问
- private 只能通过成员方法访问成员变量/函数
- protected 只有这个类型本身和其子类能访问
4.6友元
设想一下,你在一个class中声明了private属性。但是外部函数就要访问你这些私有变量。 怎么办?
我们可以声明一个’朋友’,授权给他这些访问权限
注意点:
- 友元的概念是针对类外部函数(包括全局函数和其他类的成员函数)而言的, 类自身的成员函数可以自由访问自己类中的成员。
- 但是不能把别的类的私有函数定义为友元。
- 友元函数不是该类的成员函数
- 友元函数中访问类的成员,不能直接写成员的名字,而是要借助类名来访问。
- 友元函数和类可以访问类中的所有成员变量,包括public,private,protected.
- 因为友元函数不是成员函数,也就没有this指针,所以其参数必须有一个为类对象。
- 一个函数可以被多个类声明为友元函数,这样就可以访问多个类中的 private 成员。
#include <iostream>
using namespace std;
class Player
{
private:
int gender;
bool is_over_28;
public:
Player(int gender = 0, bool is_over_28 = true);
friend void myFriend(Player &player);
};
Player::Player(int gender, bool is_over_28)
{
this->gender = gender;
this->is_over_28 = is_over_28;
};
void myFriend(Player &player)
{
cout << "form friend gender = " << player.gender << endl;
cout << "form friend is_over_28 = " << player.is_over_28 << endl;
}
int main(void)
{
Player player(13, true);
myFriend(player);
}
4.7class和struct的对比
- class的成员属性默认是private,struct的成员默认是public。
- class有构造函数和析构函数,和struct没有。
4.8初始化列表
通常情况下,我们会在构造函数中这样给变量赋初始值。
class Player
{
private:
int x, y;
public:
Player()
{
this->x = 10;
this->y = 12;
}
};
但是现在有一种更精简的写法:
class Player
{
private:
int x, y;
public:
Player(int xa = 10, int ya = 12) : x(xa), y(ya){};
};
4.8.1 初始化和赋值的对比
Player::Player(int xa = 10) : x(xa){};
这种写法叫做初始化,执行时机在调用构造函数之前
Player::Player(int xa = 10) { x = xa };
这种写法叫做赋值,执行时机在调用构造函数内部
4.9 继承
#include <iostream>
using namespace std;
class A
{
private:
int x;
public:
A(int ii) : x(ii) { cout << "A::A()" << endl; }
~A() { cout << "A::~A()" << endl; }
void print() { cout << "A::print() " << endl; }
void print(int i) { cout << " A::print(int i ) " << i << endl; }
void set(int x)
{
this->x = x;
cout << "after set x = " << this->x << endl;
}
};
class B : public A
{
public:
B() : A(15) { cout << "B::B()" << endl; };
~B() { cout << "~B::B()" << endl; }
void print(int i) { cout << "B::print() " << endl; }
};
int main(void)
{
B b;
b.print(10); // B::print()
b.set(23);
cout << "after set x = main" << b.x << endl; // 成员 "A::x" (已声明 所在行数:8) 不可访问C/C++(265)
}
和javascript的类有以下几点区别:
- 自动调用父类的构造和析构函数,不需要super()。
- 子类无法访问父类的private属性。
- 子类若想调用父类的构造函数,则必须在初始化列表里面做
- 子类和父类的构造析构函数调用顺序如下:父构造 -> 子构造 -> 子析构 -> 父析构
- name-hiding 当子类和父类存在相同的属性方法,子类会覆盖父类(直接替换)
4.10 类中的const
- 若用const修饰一个实例对象,则这个class中必须要有一个构造函数。
class A
{
int i;
public:
void f() { cout << "f()" << endl; }
void f() const { cout << "f() const" << endl; }
// A(){};
};
int main(void)
{
A const a;
// error 常量 变量 "a" 需要初始值设定项 -- 类 "A" 没有用户提供的默认构造函数C/C++(811)
a.f(); // f() const
}
- 当前存在相同名称的成员方法的时候,用const修饰的成员方法优先级更高。
- 如果成员变量用const修复,则必须通过初始化列表初始化这个变量。
4.11 多态性与虚函数
4.11.1 多态性(polymorphism)
调用同一个函数名,根据需要实现不同的功能。
- 编译时的多态性:静态多态(函数重载、运算符重载)
- 运行时的多态性:动态多态(虚函数)
函数重载和运算符重载实现的多态性属于静态多态性,在程序编译时就能决定调用的是哪个函数。静态多态性又称编译时的多态性。静态多态是通过函数重载、运算符重载实现的。
运行时的多态性是指在程序执行之前,根据函数名和参数无法确定应该调用哪一个函数,必须在程序的执行过程中,根据具体的执行情况来动态地确定。动态多态是通过虚函数(virtual function)实现的。
4.11.2 虚函数
C++中的虚函数就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数,而实现不同功能的函数。
#include <iostream>
#include <string>
using namespace std;
class Entity
{
public:
std::string GetName() { return "Entity"; }
};
class Player : public Entity
{
private:
std::string m_Name;
public:
Player(const std::string &name) : m_Name(name) {}
std::string GetName() { return m_Name; };
};
int main(void)
{
Entity *e = new Entity;
cout << e->GetName() << endl;
Player *p = new Player("Winston");
cout << p->GetName() << endl;
Entity *entity = p;
cout << entity->GetName() << endl; // entity
}
如上,我们声明了一个父类的指针,赋值为一个子类实例。当调用GetName方法的时候, 我们理所当然的认为肯定是调用在子类上面的方法。实际却调用的是父类的方法。 若想改变这种行为,我们该怎么办呢?
class Entity
{
public:
virtual std::string GetName() { return "Entity"; }
};
class Player : public Entity
{
private:
std::string m_Name;
public:
Player(const std::string &name) : m_Name(name) {}
virtual std::string GetName() override { return m_Name; };
};
int main(void)
{
Entity *e = new Entity;
cout << e->GetName() << endl;
Player *p = new Player("Winston");
cout << p->GetName() << endl;
Entity *entity = p;
cout << entity->GetName() << endl; // Winston
}
答案就是虚函数,它会维护一个虚函数列表,在运行的时候自动去分配执行子类的成员方法。
4.12 纯虚函数
C++中的纯虚函数的本质上犹如其他语言中的抽象方法
纯虚函数允许我们定义一个在基类中没有实现的函数,然后迫使在子类中实际实现。
#include <iostream>
#include <string>
using namespace std;
class Entity
{
public:
virtual std::string GetName() = 0;
};
class Player : public Entity
{
private:
std::string m_Name;
public:
Player(const std::string &name) : m_Name(name) {}
virtual std::string GetName() override { return m_Name; };
};
int main(void)
{
Entity *e = new Entity;
cout << e->GetName() << endl;
Player *p = new Player("Winston");
cout << p->GetName() << endl;
Entity *entity = p;
cout << entity->GetName() << endl;
}
在基类中定义某个成员方法 = 0,这就表明这个方法是个抽象方法。
virtual std::string GetName() = 0;
纯虚函数有以下几个特点:
- 父类将不能在被实例化(因为没有抽象方法)。
- 要实例化父类,必须在子类中实现抽象方法。
参考链接: