问题

问题 1:c++头文件为什么没有.h?

​ 在 c 语言中头文件使用扩展名.h,将其作为一种通过名称标识文件类型的简单方式。但是 c++得用法改变了,c++头文件没有扩展名。但是有些 c 语言的头文件被转换为 c++的头文件,这些文件被重新命名,丢掉了扩展名.h(使之成为 c++风格头文件),并在文件名称前面加上前缀 c(表明来自 c 语言)。例如 c++版本的 math.h 为 cmath.

​ 由于 C 使用不同的扩展名来表示不同文件类型,因此用一些特殊的扩展名(如 hpp 或 hxx)表示 c++的头文件也是可以的,ANSI/IOS 标准委员会也认为是可以的,但是关键问题是用哪个比较好,最后一致同意不适用任何扩展名。

头文件类型 约定 示例 说明
c++旧式风格 以.h 结尾 iostream.h c++程序可用
c 旧式风格 以.h 结尾 math.h c/c++程序可用
c++新式风格 无扩展名 iostream c++程序可用,使用 namespace std
转换后的 c 加上前缀 c,无扩展名 cmath c++程序可用,可使用非 c 特性,如 namespace std

问题 2:using namespace std 是什么?

​ namespace 是指标识符的各种可见范围。命名空间用关键字 namespace 来定义。命名空间是 C++的一种机制,用来把单个标识符下的大量有逻辑联系的程序实体组合到一起。此标识符作为此组群的名字。

问题 3:cout 、endl 是什么?

​ cout 是 c++中的标准输出流,endl 是输出换行并刷新缓冲区。

面向对象三大特性

封装

  • 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

  • 类将成员变量和成员函数封装在类的内部,根据需要设置访问权限,通过成员函数管理内部状态。

继承

  • 继承所表达的是类之间相关的关系,这种关系使得对象可以继承另外一类对象的特征和能力。

  • 继承的作用:避免公用代码的重复开发,减少代码和数据冗余。

多态

  • 多态性可以简单地概括为“一个接口,多种方法”,字面意思为多种形态。程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。

C++对 C 的扩展

::作用域运算符

通常情况下,如果有两个同名变量,一个是全局变量,

另一个是局部变量,那么局部变量在其作用域内具有较高的优先权,

它将屏蔽全局变量。

1
2
3
4
5
6
7
8
9
10
//全局变量
int a = 10;
//1. 局部变量和全局变量同名
void test(){
int a = 20;
//打印局部变量a
cout << "局部变量a:" << a << endl;
//打印全局变量a
cout << "全局变量a:" << ::a << endl;
}

作用域运算符可以用来解决局部变量与全局变量的重名问题,

即在局部变量的作用域内,可用::对被屏蔽的同名的全局变量进行访问。

C++命名空间(namespace)

命名空间的用途:解决名称冲突

1
2
3
4
5
6
7
8
9
10
void test(){
namespace A{
int a = 10;
}
namespace B{
int a = 20;
}
cout << "A::a : " << A::a << endl;
cout << "B::a : " << B::a << endl;
}

命名空间可以嵌套

1
2
3
4
5
6
7
8
9
namespace A{
int a = 10;
namespace B{
int a = 20;
}
}
void test(){
cout << "A::a : " << A::a << endl;
cout << "A::B::a : " << A::B::a << endl;

命名空间下可以存储 变量, 函数,结构体,类…..

1
2
3
4
5
6
7
namespace A
{
int m_A = 10;
void func(){}
struct Person{};
class Animal{};
}
  • 函数的声明和实现可分离
1
2
3
4
5
6
namespace MySpace{
void func2(int param);
}
void MySpace::func2(int param){
cout << "MySpace::func2 : " << param << endl;
}

命名空间必须声明在全局作用域下

1
2
3
4
void test02()
{
//namespace B //不能定义在局部中
}

命名空间是开放的,可以随时向空间中添加新成员

1
2
3
4
5
6
namespace B{
int a = 20;
}
namespace B{
int b = 20;
}

命名空间可以是匿名的,只能在本文件内访问

1
2
3
4
5
6
7
namespace
{
int a = 20;
int b = 20;
}
cout << a;
cout << ::b;

命名空间可以起别名

1
2
3
4
5
6
7
namespace veryLongName{

int a = 10;
void func(){ cout << "hello namespace" << endl; }
}
namespace shortName = veryLongName;
cout << "veryLongName::a : " << shortName::a << endl;

using

using 声明

1
2
3
4
5
6
7
8
9
10
11
namespace LOL
{
int sunWuKongId = 1;
}
void test01()
{
//int sunWuKongId = 2; 会报错
//using声明和就近原则不要同时出现,尽量避免这种情况
using LOL::sunWuKongId;
cout << sunWuKongId <<endl;
}

using 编译指令

1
2
3
4
//int sunWuKongId = 2; 会输出2, 就近原则
using namespace LOL;
cout << sunWuKongId <<endl;
//using编译指令和就近原则同时出现,优先使用就近原则
1
2
3
4
5
//LOL和KingGlory里都有sunWuKongId
using namespace LOL;
using namespace KingGlory;
cout << LOL::sunWuKongId <<endl;
//当出现多个编译指令,并且出现同名情况,使用数据依然加zon

内联函数

内联函数引出

在 c 中我们经常把一些短并且执行频繁的计算写成宏,而不是函数,这样做的理由是为了执行效率,宏可以避免函数调用的开销,这些都由预处理来完成。

但是在 c++出现之后,使用预处理宏会出现两个问题:

  • 第一个在 c 中也会出现,宏看起来像一个函数调用,但是会有隐藏一些难以发现的错误。

  • 第二个问题是 c++特有的,预处理器不允许访问类的成员,也就是说预处理器宏不能用作类类的成员函数。

为了保持预处理宏的效率又增加安全性,而且还能像一般成员函数那样可以在类里访问自如,c++引入了内联函数(inline function).

内联函数为了继承宏函数的效率,没有函数调用时开销,然后又可以像普通函数那样,可以进行参数,返回值类型的安全检查,又可以作为成员函数。

预处理宏的缺陷

问题一:需要加括号保证运算的完整

1
2
3
4
5
6
7
8
9
10
11
12
13
#define ADD(x,y) x+y
inline int Add(int x,int y){
return x + y;
}
void test(){
int ret1 = ADD(10, 20) * 10; //希望的结果是300
//展开变成了 10 + 20 * 10
int ret2 = Add(10, 20) * 10; //希望结果也是300
cout << "ret1:" << ret1 << endl; //210
cout << "ret2:" << ret2 << endl; //300
}
//解决方案, 需要加括号保证运算的完整
#define ADD(x,y) ((x) + (y))

问题二:即使加了括号,有些情况依然与预期效果不符

1
2
3
4
5
6
7
8
9
10
11
#define COMPARE(x,y) ((x) < (y) ? (x) : (y))
int Compare(int x,int y){
return x < y ? x : y;
}
void test02(){
int a = 1;
int b = 3;
//cout << "COMPARE(++a, b):" << COMPARE(++a, b) << endl; // 3
cout << "Compare(int x,int y):" << Compare(++a, b) << endl; //2
}
//因为宏展开后是((++a) < (b) ? (++a) : (b))

内联函数基本概念

在 c++中,预定义宏的概念是用内联函数来实现的,而内联函数本身也是一个真正的函数。内联函数具有普通函数的所有行为。唯一不同之处在于内联函数会在适当的地方像预定义宏一样展开,所以不需要函数调用的开销。因此应该不使用宏,使用内联函数。

  • 在普通函数(非成员函数)函数前面加上 inline 关键字使之成为内联函数。
  • 但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。
1
inline void func(int a);

以上写法没有任何效果,仅仅是声明函数,应该如下方式来做:

1
inline int func(int a){return ++;}
  • 注意: 编译器将会检查函数参数列表使用是否正确,并返回值(进行必要的转换)。这些事预处理器无法完成的。

  • 内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间

总结:

内联函数优点

解决了宏的缺陷,因为是普通函数,又带来了宏的优点,以空间换时间

关键字 inline

函数的声明和函数的实现同时拥有 inline 才算内联

类内部的内联函数

类内部的成员函数,都隐藏的加了 inline 关键字

任何在类内部定义的函数自动成为内联函数。

1
2
3
4
5
class Person{
public:
Person(){ cout << "构造函数!" << endl; }
void PrintPerson(){ cout << "输出Person!" << endl; }
}

内联函数和编译器

c++内联编译会有一些限制,以下情况编译器可能考虑不会将函数进行内联编译:

  • 不能存在任何形式的循环语句
  • 不能存在过多的条件判断语句
  • 函数体不能过于庞大
  • 不能对函数进行取址操作

内联仅仅只是给编译器一个建议,编译器不一定会接受这种建议,如果你没有将函数声明为内联函数,那么编译器也可能将此函数做内联编译。一个好的编译器将会内联小的、简单的函数。

引用

1
2
int a = 5;
int& ref = a;

ref 并不是一个变量,只是 a 的别名,并不占用内存空间

引用很容易与指针混淆,它们之间有三个主要的不同:

  • 不存在空引用。引用必须连接到一块合法的内存。
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
  • 引用必须在创建时被初始化。指针可以在任何时间被初始化。

把引用作为参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

void swap(int& x, int& y);

int main ()
{
// 局部变量声明
int a = 100;
int b = 200;

swap(a, b);

return 0;
}

// 函数定义
void swap(int& x, int& y)
{
int temp;
temp = x; /* 保存地址 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 x 赋值给 y */

return;
}

字符串

stringstream 用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <sstream>

using namespace std;

int main()
{
string a, b, c;

getline(cin, a);
cin >> b >> c;

stringstream ssin(a);
string str;
while (ssin >> str)
if (str == b) cout << c << ' ';
else cout << str << ' ';
return 0;
}

stringstream ssin(a); 将字符串初始化成字符串流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// swapping ostringstream objects
#include <string> // std::string
#include <iostream> // std::cout
#include <sstream> // std::stringstream

int main () {

std::stringstream ss;

ss << 100 << ' ' << 200;

int foo,bar;
ss >> foo >> bar;

std::cout << "foo: " << foo << '\n';
std::cout << "bar: " << bar << '\n';

return 0;
}

Output:

foo: 100
bar: 200

从 string 对象 str 中读取字符。遇空格结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <sstream>

using namespace std;

int main()
{
string str = "hello world";
cout << str << endl;

stringstream ss(str); //将str复制到ss
string abc;
while(ss >> abc) //相当于输入一个个的单词
{
cout << abc <<endl;
}

return 0;
}

OUTPUT:

hello world

hello

world

结构体

若使用普通的变量来存储的话,数据是分散的。

1
2
3
4
char name[20];
int gender;
double height;
double weight;

但是,使用 struct 关键词,可以聚合这几种数据类型成为一个新的数据类型。

1
2
3
4
5
struct{
char name[20];
int gender;
double height;
double weight;}

上面这一串结构类型虽然很长,但是,就相当于 int 类型一样。 如同在 int 后填变量名可以声明一个整型变量。 在结构类型后面填写变量名可以声明一个结构变量。

1
2
3
4
5
6
7
int n;
struct {
char name[20];
int gender;
double height;
double weight;
}timmy;

timmy 是由该结构声明的变量,它由 4 个成员组成。

使用成员运算符 . 加上 字段名 可以访问到结构的各个成员。

timmy.name;

timmy.gender;

timmy.height;

timmy.weight;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct {
char name[20];
int gender;
double height;
double weight;
}timmy;
// 将Timmy的数据装入结构变量
strcpy(timmy.name, "Timmy");
timmy.gender = 1;
timmy.height = 170.00;
timmy.weight = 60;
// 从结构变量中显示Timmy的数据
printf("%s\n", timmy.name);
printf("%d\n", timmy.gender);
printf("%.2f\n", timmy.height);
printf("%.2f\n", timmy.weight);

结构别名

现在,我们想定义多个人员信息结构变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct {
char name[20];
int gender;
double height;
double weight;
}timmy;

struct {
char name[20];
int gender;
double height;
double weight;
}david;

struct {
char name[20];
int gender;
double height;
double weight;
}jane;

由于这几个结构变量的内部成员都是一致的,能不能只声明一次结构类型,后续继续使用呢? 我们可以给结构类型声明取一个别名。

1
2
3
4
5
6
7
8
struct person{
char name[20];
int gender;
double height;
double weight;
}timmy;
struct person david;
struct person jane;

在第一次声明结构变量时,在 struct{之间可以填写一个结构别名

若以后再次需要使用这种结 构,仅需要使用 struct 加 别名 即可声明这种结构的变量。

事实上,我们可以将结构类型声明提取到最开头。让所有的结构变量均由别名来声明。相当于我们先造 了一个模板,然后,用这个模板生成各个变量。

1
2
3
4
5
6
7
8
9
struct person{
char name[20];
int gender;
double height;
double weight;
};
struct person timmy;
struct person david;
struct person jane;

请注意,如果结构类型声明在一个函数中,那么别名只能在函数内部使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void func1()
{
struct person{
char name[20];
int gender;
double height;
double weight;
};
struct person timmy;
}
void func2()
{
// 别名person无法在func2中使用
struct person david;
}

函数 func1 声明了一个结构类型,它的别名为 person 。并且使用别名声明了一个结构变量 timmy 。

函数 func2 中,使用别名 person ,声明另一个结构变量,但是别名 person 无法在函数 func2 中使用, 因此将编译报错。

如果需要在多个函数中使用结构别名,那么可以把它放到函数外面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct person{
char name[20];
int gender;
double height;
double weight;
};
void func1()
{
struct person timmy;
}
void func2()
{
struct person david;
}

初始化结构

1
2
3
4
5
6
7
8
9
10
11
struct person{
char name[20];
int gender;
double height;
double weight;
};
struct person timmy = {"timmy", 1, 170.00, 60.00};
printf("%s\n", timmy.name);
printf("%d\n", timmy.gender);
printf("%.2f\n", timmy.height);
printf("%.2f\n", timmy.weight);

结构的初始化列表的写法需要注意如下 4 点:

  • 初始化列表由花括号包括。
  • 花括号内为结构成员需要被初始化的值。
  • **初始化值按照结构成员声明时的顺序依次排列 **
  • 每个初始化值之间由逗号分隔。

对于第三点, person 结构成员声明的顺序依次为 name 、 gender 、 height 、 weight 。 对应的初始化列表中的初始化值顺序为”timmy”、 1、170.00、 60.00。 需要严格地对应顺序。

结构数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct person{
char name[20];
int gender;
double height;
double weight;
};
struct person people[3] = {
{"timmy", 1, 170.00, 60.00},
{"david", 1, 175.00, 65.00},
{"jane", 2, 165.00, 55.00}
};
for(int i = 0; i < 3; i ++)
{
struct person per = people[i];
printf("%s ", per.name);
printf("%d ", per.gender);
printf("%.2f ", per.height);
printf("%.2f\n", per.weight);
}

嵌套结构

一个结构可以作为另一个结构的成员。

例如,我们声明一个结构,用于存储通讯方式。通讯方式由电话号码,邮箱组成。

1
2
3
4
struct contact {
char phone[20];
char email[20];
};

现在,我们需要记录每个人员的通讯方式。可以把这个结构作为人员结构的成员。

1
2
3
4
5
6
7
struct person{
char name[20];
int gender;
double height;
double weight;
struct contact c;
};

初始化:

1
2
3
struct person timmy = {
"timmy", 1, 170.00, 60.00, {"130123456678", "timmy@xxx.com"}
};

使用 . 加 字段名 可以访问到通讯方式结构。但是,你肯定还想访问其内部的成员,再次使 用 . 加 字段名 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct contact {
char phone[20];
char email[20];
};
struct person{
char name[20];
int gender;
double height;
double weight;
struct contact c;
};
struct person timmy = {
"timmy", 1, 170.00, 60.00, {"130123456678", "timmy@xxx.com"}
};
printf("%s ", timmy.name);
printf("%d ", timmy.gender);
printf("%.2f ", timmy.height);
printf("%.2f\n", timmy.weight);
printf("%s\n", timmy.c.phone);
printf("%s\n", timmy.c.email);

指向结构的指针

1
2
3
4
5
6
7
8
struct person{
char name[20];
int gender;
double height;
double weight;
};
struct person timmy = {"timmy", 1, 170.00, 60.00};
struct person *pTimmy = &timmy;

由于取地址 & 与取值 * 它们具有可逆关系,我们可以把指针先转为结构再使用。

1
2
3
4
printf("%s\n", (*pTimmy).name);
printf("%d\n", (*pTimmy).gender);
printf("%.2f\n", (*pTimmy).height);
printf("%.2f\n", (*pTimmy).weight);

由于成员运算符 . 的优先级高于取值 _ 。为了让取值 _ 先运算符,必须使用括号 *pTimmy 包括。

另外,C 语言中提供了更加方便的写法,成员间接运算符 ->

(*pTimmy).name 等价于 pTimmy->name

结构在函数中传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct person{
char name[20];
int gender;
double height;
double weight;
};
void change(struct person per)
{
strcpy(per.name, "david");
per.gender = 1;
per.height = 175.00;
per.weight = 65.00;
}
int main()
{
struct person timmy = {"timmy", 1, 170.00, 60.00};
change(timmy);
printf("%s\n", timmy.name);
printf("%d\n", timmy.gender);
printf("%.2f\n", timmy.height);
printf("%.2f\n", timmy.weight);
return 0;
}

现在,我们将结构当作参数传入函数。在函数内部修改传入的参数。 很显然,由于实参 timmy 与实参 per 是相互独立的。修改函数 change 内的 per 无法改动实参 timmy 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct person{
char name[20];
int gender;
double height;
double weight;
};
void change(struct person *per)
{
strcpy(per->name, "david");
per->gender = 1;
per->height = 175.00;
per->weight = 65.00;
}
int main()
{
struct person timmy = {"timmy", 1, 170.00, 60.00};
change(&timmy);
printf("%s\n", timmy.name);
printf("%d\n", timmy.gender);
printf("%.2f\n", timmy.height);
printf("%.2f\n", timmy.weight);
return 0;
}

但是,如果将 change 函数的参数改为指向结构的指针,情况就大不相同了。在函数 change 内部可以通 过指针,找到结构变量 timmy 。并且,对其进行修改

最后,将一个结构从函数返回也是可以的。从函数返回了 david 的数据,并且在将其赋值给了 timmy。

1
2
3
4
5
6
7
8
9
10
11
12
struct person change()
{
struct person per;
strcpy(per.name, "david");
per.gender = 1;
per.height = 175.00;
per.weight = 65.00;
return per;
}

struct person timmy = {"timmy", 1, 170.00, 60.00};
timmy = change();

类中的变量和函数被统一称为类的成员变量。

private后面的内容是私有成员变量,在类的外部不能访问;public后面的内容是公有成员变量,在类的外部可以访问。

类的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>

using namespace std;

const int N = 1000010;

class Person
{
private:
int age, height;
double money;
string books[100];

public:
string name;

void say()
{
cout << "I'm " << name << endl;
}

int set_age(int a)
{
age = a;
}

int get_age()
{
return age;
}

void add_money(double x)
{
money += x;
}
} person_a, person_b, persons[100];

int main()
{
Person c;

c.name = "yxc"; // 正确!访问公有变量
c.age = 18; // 错误!访问私有变量
c.set_age(18); // 正确!set_age()是共有成员变量
c.add_money(100);

c.say();
cout << c.get_age() << endl;

return 0;
}

生成变量特殊方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class player {
int x, y;
int speed;
void move(int a, int b){
x += a * speed;
y += b * speed;
}
}c, player[1000]; //生成变量

int main()
{
player players[1000]; //生成变量
return 0;
}

类默认私有

1
2
3
4
5
6
7
8
class player {
int x, y;
int speed;
void move(int a, int b){
x += a * speed;
y += b * speed;
}
};

访问控制(private、public)

1
2
3
4
5
6
7
8
9
10
class player {
public:
int x, y;
int speed;
private:
void move(int a, int b){
x += a * speed;
y += b * speed;
}
};

C++类与结构体对比

区别:

作用上:class 默认 private,struct 默认 public。
使用上:引入 struct 是为了让 C++向后兼容 C。

推荐选用:

若只包含一些变量结构或 POD(plain old data)时,选用 struct。例

如数学中的向量类。

1
2
3
4
5
6
7
struct Vec2{
float x, y;
void Add(const Vec2& other){
x += other.x;
y += other.y;
}
};

若要实现很多功能的类,则选用 class

若是只有数据,函数较少的,用 struct