一.类的基本概念
类是一些列属性数据和事务行为的封装,是C++面向对象的核心基础。类是对象的抽象,对象是类的具体化;类的基本内容包括事物的属性(静态数据字段)和行为(动态函数方法),类中通过 private(私有,仅类内部可访问)、public(公有,任意位置可访问)、protected(保护,仅类内部和所有子类内部可访问) 标记访问区域,成员和类的默认访问修饰符是 private。
1. 构造函数与析构函数
一.构造函数:构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。其特点如下:
1.名称与类的名称是完全相同的,并且不会返回任何类型 Object()
2.构造函数可以带有参数,在创建对象时赋初值
-代码段初始化:Object(param1,param2...){a = param1;...}
-初始化列表初始化:Object(param1,param2...):a(param1),... {}
3.一个类可以有多个不同的析构函数,但是参数列表必须不同。在构建对象时动态选择执行
4.若没有声明构造函数,则系统会自动生成默认的无参构造函数,函数为空,什么都不做二.析构函数:析构函数是类的一种特殊的成员函数,它会在每次销毁所创建的对象时自动执行。
- 特点:
1.名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数
2.析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
3.如果用户没有定义,编译器会自动生成一个默认的析构函数,什么也不干- 执行时机:
1.在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。
2.在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。
3.new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行。
#include<iostream>
using namespace std;
class Box {
//private、public、protected标记访问区域,成员和类的默认访问修饰符是 private。
private:
double length;
double breadth;
double height;
public:
int id;
//构造函数:构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
//特点:
// 1.名称与类的名称是完全相同的,并且不会返回任何类型 Object()
// 2.构造函数可以带有参数,在创建对象时赋初值
// -代码段初始化:Object(param1,param2...){a = param1;...}
// -初始化列表初始化:Object(param1,param2...):a(param1),... {}
// 3.一个类可以有多个不同的析构函数,但是参数列表必须不同。在构建对象时动态选择执行
// 4.若没有声明构造函数,则系统会自动生成默认的无参构造函数,函数为空,什么都不做
Box() {
cout << "Object [" << id << "] is being created" << endl;
}
Box(int id,double len, double bre, double hei):id(id),length(len),breadth(bre),height(hei){
cout << "Object["<<id<< "] is init by length = " << length << ", breadth = " << breadth << ", height = " << height << endl;
}
//析构函数:析构函数是类的一种特殊的成员函数,它会在每次销毁所创建的对象时自动执行
//特点:
// 1.名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数
// 2.析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
// 3.如果用户没有定义,编译器会自动生成一个默认的析构函数,什么也不干
//执行时机:
// 1.在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。
// 2.在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。
// 3.new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行。
~Box(){
cout << "Object["<<id<<"] is being distoryed" << endl;
}
double getLength() {
return length;
}
void setLength(double len) {
length = len;
}
double getBreadth() {
return breadth;
}
void setBreadth(double breadth) {
this->breadth = breadth;
}
double getHeight() {
return height;
}
void setHeight(double high) {
height = high;
}
//成员函数 先声明
double getVolume();
};
//外部成员函数定义
double Box::getVolume() {
return length * breadth * height;
}
int main() {
//声明变量 -> 程序自动在栈空间分配内存
//调用无参构造函数
Box box1;
box1.id = 0;
box1.setLength(6.0);
box1.setBreadth(7.0);
box1.setHeight(5.0);
cout << "Box1 的体积为:" << box1.getVolume() << endl;
//调用有参构造函数 等价于 Box box2 = Box(2,3,4);
Box box2(1,2, 3, 4);
cout << "Box2 的体积为:" << box2.getVolume() << endl;
return 0;
}
2.拷贝构造函数
一.拷贝构造函数:拷贝构造函数是一种特殊的构造函数,它在创建对象时,在使用同一类中之前创建的对象来初始化新创建的对象时自动调用
- 拷贝函数调用场景:
1.通过使用另一个同类型的对象来 初始化新创建的对象。Object o1 = o2;
2.作为参数传递给函数
3.作为函数返回值
- 特点:
1.函数形式:参数常量引用 classname (const classname &obj) {}(因为传值的话会造成无限制递归调用拷贝复制)
2.如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。
#include<iostream>
using namespace std;
class Box {
//private、public、protected标记访问区域,成员和类的默认访问修饰符是 private。
private:
double length;
double breadth;
double height;
public:
int id;
//构造函数:构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
Box() {
cout << "Object [" << id << "] is being created" << endl;
}
Box(int id,double len, double bre, double hei):id(id),length(len),breadth(bre),height(hei){
cout << "Object["<<id<< "] is init by length = " << length << ", breadth = " << breadth << ", height = " << height << endl;
}
//析构函数:析构函数是类的一种特殊的成员函数,它会在每次销毁所创建的对象时自动执行
~Box(){
cout << "Object["<<id<<"] is being distoryed" << endl;
}
//拷贝构造函数:拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象时自动调用
//拷贝函数调用场景:
// 1.通过使用另一个同类型的对象来 初始化新创建的对象。Object o1 = o2;
// 2.作为参数传递给函数
// 3.作为函数返回值
//特点:
// 1.形式:常量引用 classname (const classname &obj) {}(因为传值的话会造成无限制递归调用拷贝复制)
// 2.如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。
Box(const Box &another) {
cout << "object[" << id << "] 调用拷贝构造函数" << endl;
this->id = another.id;
//拷贝构造函数内可以直接访问本类任何对象任何成员变量
this->length = another.length;
this->breadth = another.breadth;
this->height = another.height;
}
double getLength() {
return length;
}
void setLength(double len) {
length = len;
}
double getBreadth() {
return breadth;
}
void setBreadth(double breadth) {
this->breadth = breadth;
}
double getHeight() {
return height;
}
void setHeight(double high) {
height = high;
}
//成员函数 先声明
double getVolume();
};
//外部成员函数定义
double Box::getVolume() {
return length * breadth * height;
}
//执行拷贝构造函数1:函数参数传值拷贝 => Box box = box2;
Box resizeBox(Box box) {
cout << "执行 function resizeBox" << endl;
box.id = 3;
box.setLength(6);
box.setBreadth(6);
box.setHeight(6);
return box;
//执行拷贝构造函数2:函数返回值拷贝 => Box temple = box;
//执行析构函数销毁box
}
//返回引用
Box& resizeBox2(Box box) {
cout << "执行 function resizeBox2" << endl;
box.id = 3;
box.setLength(6);
box.setBreadth(6);
box.setHeight(6);
return box;
//先销毁box局部变量,调用析构函数
//返回引用,由于局部变量已经销毁,所以为空!
}
int main() {
//声明变量 -> 程序自动在栈空间分配内存
//调用无参构造函数
Box box1;
box1.id = 0;
box1.setLength(6.0);
box1.setBreadth(7.0);
box1.setHeight(5.0);
cout << "Box1 的体积为:" << box1.getVolume() << endl;
//调用有参构造函数 等价于 Box box2 = Box(2,3,4);
Box box2(1,2, 3, 4);
cout << "Box2 的体积为:" << box2.getVolume() << endl;
//初始化对象调用拷贝构造函数,初始化变量 等价于 Box box3(box2);
Box box3 = box2;
cout << "Box3 的体积为:" << box3.getVolume() << endl;
//函数调用拷贝构造函数 情况1
//1.编译器优化:函数返回值赋值初始化创建对象时,只执行一次拷贝构造函数
//2.这种写法临时对象空间temple没有在函数调用结束释放,而是对象box4的地址一样,在main函数结束时一起释放
Box box4 = resizeBox(box2);
cout << "Box4 的体积为:" << box4.getVolume() << endl;
//函数调用拷贝构造函数 情况2
//1.只执行一次拷贝构造函数
//2.临时对象空间temple在函数调用结束释放
resizeBox(box2);
//引用返回值:赋值时又会调用拷贝构造函数(但是函数内局部变量已经被销毁了,引用为0地址)
Box box5 = resizeBox2(box2);
cout << "Box5 的体积为:" << box5.getVolume() << endl;
return 0;
}
二.多 态
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。多态现在一般的用法,就是拿一个父类的指针去调用子类中被重写的方法。
1.虚函数
一.虚函数
1.定义:虚函数 是在基类中使用关键字 virtual 声明的函数。
2.特点:
(1)非虚函数重写:静态链接或早绑定。在程序编译期间,编译器只能根据指针的类型判断所指向的对象,从而调用其方法(非多态)
(2)虚函数重写:动态链接,或后期绑定。其在程序运行期间动态判断所指对象,并执行方法,其流程如下:
1.在类中,用 virtual 声明一个函数时,就会在这个类中对应产生一张 虚函数表,将虚函数存放到该表中;
2. 用这个类创建对象时,就会产生一个 vptr指针,这个vptr指针会指向对应的虚函数表;
3. 在多态调用时, vptr指针 就会根据这个对象 在对应类的虚函数表中 查找被调用的函数,从而找到函数的入口地址;
》 如果这个对象是 子类的对象,那么vptr指针就会在 子类的 虚函数表中查找被调用的函数
》 如果这个对象是 父类的对象,那么vptr指针就会在 父类的 虚函数表中查找被调用的函数
3.实现多态的条件:
(1). 要有继承
(2). 要有虚函数重写(被 virtual 声明的函数叫虚函数)
(3). 要有父类指针(父类引用)指向子类对象二.new与Delete
1.作用:new 是c++的一个关键字,用于分配内存,返回指针
2.原理:new 执行时首先在堆上开辟一段新的空间,并执行初始化构造函数,并返回首地址指针
3.与声明变量区别:声明变量在栈上分配内存,由系统管理和回收。new 在堆上分配内存,由程序员手动管理和释放(delete)
4.与malloc区别:malloc搭配free使用,也在堆上动态申请内存,属于c的关键字,但是malloc只申请内存,不进行初始化,new更智能。
#include<iostream>
using namespace std;
//一.虚函数
// 1.定义:虚函数 是在基类中使用关键字 virtual 声明的函数。
// 2.特点:
// -非虚函数重写:静态链接或早绑定。在程序编译期间,编译器只能根据指针的类型判断所指向的对象,从而调用其方法(非多态)
// -虚函数重写:动态链接,或后期绑定。其在程序运行期间动态判断所指对象,并执行方法,其流程如下:
// 1. 在类中,用 virtual 声明一个函数时,就会在这个类中对应产生一张 虚函数表,将虚函数存放到该表中;
// 2. 用这个类创建对象时,就会产生一个 vptr指针,这个vptr指针会指向对应的虚函数表;
// 3. 在多态调用时, vptr指针 就会根据这个对象 在对应类的虚函数表中 查找被调用的函数,从而找到函数的入口地址;
// 》 如果这个对象是 子类的对象,那么vptr指针就会在 子类的 虚函数表中查找被调用的函数
// 》 如果这个对象是 父类的对象,那么vptr指针就会在 父类的 虚函数表中查找被调用的函数
// 3.实现多态的条件:
// 1. 要有继承
// 2. 要有虚函数重写(被 virtual 声明的函数叫虚函数)
// 3. 要有父类指针(父类引用)指向子类对象
class Shape {
protected:
int width, height;
public:
Shape() {
width = 0;
height = 0;
cout << "Shape is created" << endl;
}
Shape(int w = 0, int h = 0) :width(w), height(h) {
cout << "Shape is created" << endl;
}
//虚函数
virtual int getArea() {
cout << "Shape class Area" << endl;
return 0;
}
//非虚函数
void print() {
cout << "This is a Shape" << endl;
}
virtual ~Shape() {
cout << "Shape is distoryed" << endl;
}
};
class Rectangle :public Shape {
public:
Rectangle(int wid,int hei):Shape(wid,hei) {
cout << "Rectangle is created" << endl;
}
//虚函数重写:动态链接(多态)
int getArea() {
cout << "Rectangle class Area is "<< width*height<< endl;
return width*height;
}
//非虚函数重写:静态链接
void print() {
cout << "This is a Rectangle" << endl;
}
~Rectangle() {
cout << "Rectangle is distoryed" << endl;
}
};
class Triangle :public Shape {
public:
Triangle(int wid, int hei) :Shape(wid, hei) {
cout << "Triangle is created" << endl;
}
//虚函数重写:动态链接(多态)
int getArea() {
cout << "Triangle class Area is " << (width * height)/2 << endl;
return (width * height)/2;
}
//非虚函数重写:静态链接
void print() {
cout << "This is a Triangle" << endl;
}
~Triangle() {
cout << "Triangle is distoryed" << endl;
}
};
int main() {
//1.父类型创建父对象:调用父类方法
Shape shape = Shape(3, 4);
shape.getArea();
shape.print();
//2.父类型创建子对象:分为两步,首先创建Rectangle对象,然后赋值给Shape,最终还是调用父类方法。(无法实现多态)
// 注意:要想实现多态,只能通过 [父指针指向子类],或者[父引用指向子类]
// 原因:
// 1.指针和引用类型只是要求了基地址和这种指针所指对象的内存大小,与对象的类型无关,相当于把指向的内存解释成指针或引用的类型。
// 2.而把一个派生类对象直接赋值给基类对象,就牵扯到对象的类型问题,编译器就会回避之前的的虚机制。从而无法实现多态
Shape rect = Rectangle(3, 4);
rect.getArea();
rect.print();
//3.父指针指向子类
// 1.getArea是虚函数,动态调用子类实现
// 2.print不是虚函数,静态链接,调用的是父类Shape的方法
// 3.析构函数不是虚函数,静态链接,调用释放的是父类Shape的资源
Shape* rect2 = new Rectangle(3, 4);
rect2->getArea();
rect2->print();
delete rect2;
//4.父类引用指向子类对象
// 1.getArea是虚函数,动态调用子类实现
// 2.print不是虚函数,静态链接,调用的是父类Shape的方法
// 3.析构函数不是虚函数,静态链接。但是引用是Rectangle的别名,调用子类析构函数=>父类析构函数
Rectangle rect3 = Rectangle(3, 4);
Shape& p_rect3 = rect3;
p_rect3.getArea();
p_rect3.print();
//5.虚析构函数:virtual ~Object(){}
// 会调用子类析构函数 => 父类析构函数
Shape* rect4 = new Rectangle(3, 4);
rect4->getArea();
rect4->print();
delete rect4;
//6.new与delete
// 1.作用:new 是c++的一个关键字,用于分配内存,返回指针
// 2.原理:new 执行时首先在堆上开辟一段新的空间,并执行初始化构造函数,并返回首地址指针
// 3.与声明变量区别:声明变量在栈上分配内存,由系统管理和回收。new 在堆上分配内存,由程序员手动管理和释放(delete)
// 4.与malloc区别:malloc搭配free使用,也在堆上动态申请内存,属于c的关键字,但是malloc只申请内存,不进行初始化,new更智能。
return 0;
}
2.纯虚函数
一.纯虚函数
1.定义:virtual double getVolume() = 0; = 0 就是告诉编译器,函数没有主体,此虚函数是纯虚函数。
2.特点:
(1)如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。
(2)如果类中所有函数都被声明为纯虚函数,则这个类就是接口。
(3)抽象类或接口不能被实例化,只能通过多态指向子类
(4)若子类继承抽象类或接口,则子类必须实现所有的纯虚函数,否则子类也成为抽象类
3.注意:c++区别于java,没有abstract、interface关键字,所有的多态都通过virtual 实现
//二.纯虚函数
// 1.定义:virtual double getVolume() = 0; = 0 就是告诉编译器,函数没有主体,此虚函数是纯虚函数。
// 2.特点:
// (1)如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。
// (2)如果类中所有函数都被声明为纯虚函数,则这个类就是接口。
// (3)抽象类或接口不能被实例化,只能通过多态指向子类
// (4)若子类继承抽象类或接口,则子类必须实现所有的纯虚函数,否则子类也成为抽象类
// 3.注意:c++区别于java,没有abstract、interface关键字,所有的多态都通过virtual 实现
class Shape {
protected:
int width, height;
public:
Shape() {
width = 0;
height = 0;
cout << "Shape is created" << endl;
}
Shape(int w = 0, int h = 0) :width(w), height(h) {
cout << "Shape is created" << endl;
}
//虚函数
virtual int getArea() = 0;
//非虚函数
void print() {
cout << "This is a Shape" << endl;
}
virtual ~Shape() {
cout << "Shape is distoryed" << endl;
}
};
class Rectangle :public Shape {
public:
Rectangle(int wid, int hei) :Shape(wid, hei) {
cout << "Rectangle is created" << endl;
}
//虚函数重写:动态链接(多态)
int getArea() {
cout << "Rectangle class Area is " << width * height << endl;
return width * height;
}
//非虚函数重写:静态链接
void print() {
cout << "This is a Rectangle" << endl;
}
~Rectangle() {
cout << "Rectangle is distoryed" << endl;
}
};
int main() {
//纯虚函数=>抽象类
Shape* rect2 = new Rectangle(3, 4);
rect2->getArea();
rect2->print();
delete rect2;
}
三.结构体
1.定义:结构体类似于类,是一种数据集合的数据结构,比类轻量级
2.结构体与类的区别:
-使用关键字 struct 而不是关键字 class。
-尽管结构体可以包含成员函数,但它们很少这样做。所以,通常情况下结构体声明只会声明成员变量。
-结构体声明通常不包括 public 或 private 的访问修饰符。
-类成员默认情况是私有的,而结构体的成员则默认为 public。程序员通常希望它们保持公开,只需使用默认值即可
#include<iostream>
#include<algorithm>
#include<stdio.h>
using namespace std;
//结 构 体
// 1.定义:结构体类似于类,是一种数据集合的数据结构,比类轻量级
// 2.区别:
// -使用关键字 struct 而不是关键字 class。
// -尽管结构体可以包含成员函数,但它们很少这样做。所以,通常情况下结构体声明只会声明成员变量。
// -结构体声明通常不包括 public 或 private 的访问修饰符。
// -类成员默认情况是私有的,而结构体的成员则默认为 public。程序员通常希望它们保持公开,只需使用默认值即可
//char 字符串初始化:
// 1.使用char *str时,需要搭配new char[10];来开辟空间,要不str只是指向一个随即地址
// 2.使用char str[10],会初始化好长度和地址
struct Student {
int sdu_id;
char *sdu_name;
int sdu_age;
int sdu_sumGrade;
Student() {
sdu_name = new char[10];
}
Student(int id, char name[], int age, int sumGrade) :sdu_id(id), sdu_name(name), sdu_age(age), sdu_sumGrade(sumGrade) {
cout << "Student is created" << endl;
}
//重载<号,<为true,sort默认升序排序
//bool operator<(const Student& another) {
// return sdu_id < another.sdu_id;
//}
//重载<号,>为true,sort默认降序排序
bool operator<(const Student& another) {
return sdu_id > another.sdu_id;
}
};
//自定义sort排序函数(如果使用了排序函数,则忽略类或结构体内部的比较重载)
bool cmp(const Student& sdu1,const Student& sdu2) {
//如果sumGrade相同,按照id升序排列
if (sdu1.sdu_sumGrade == sdu2.sdu_sumGrade)return sdu1.sdu_id < sdu2.sdu_id;
//否则 按照sumGrade降序排列
return sdu1.sdu_sumGrade > sdu2.sdu_sumGrade;
}
int main() {
//初始化结构体数组,会默认调用无参构造函数
Student students[100];
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
//cin >> students[i].sdu_id >> students[i].sdu_name >> students[i].sdu_age >> students[i].sdu_sumGrade;
scanf("%d%s%d%d", &students[i].sdu_id, students[i].sdu_name, &students[i].sdu_age, &students[i].sdu_sumGrade);
}
for (int i = 0; i < n; i++) {
printf("Student[%d]:%s %d %d\n", students[i].sdu_id, students[i].sdu_name, students[i].sdu_age, students[i].sdu_sumGrade);
}
//sort函数:本质是快排(不稳定的nlogn)
//sort(begin,end,cmp) 开始地址,结束地址,比较方法(默认为升序排序)
//注意:如果是类或者结构体,则会调用其内部的大小比较方法(重载<号)。其他的基本类型可以外部使用cmp自定义排序
//sort(students, students + n,cmp);
sort(students, students + n);
for (int i = 0; i < n; i++) {
printf("Student[%d]:%s %d %d\n", students[i].sdu_id, students[i].sdu_name, students[i].sdu_age, students[i].sdu_sumGrade);
}
}
四.预处理器指令与宏定义
1.#define 宏定义
1.预处理器:预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。
2.书写格式:所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。预处理指令不是 C++ 语句,所以它们不会以分号(; )结尾。3.#define 预处理指令:#define 预处理指令用于[创建符号常量]。该符号常量通常称为宏
(1)无参宏:#define name value,在该文件中后续出现的所有宏都将会在程序编译之前被替换为value
(2)参数宏:#define name(参数) 运算表达式,替换函数表达
4.注意:宏定义只做替换,不做运算。一个标识符被宏定义后,该标识符便是一个宏名。这时,在程序中出现的是宏名,在该程序被编译前,先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译,宏替换是简单的替换。
#include <iostream>
#include <cmath>
using namespace std;
//1.预处理器:预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。
//2.书写格式:所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。预处理指令不是 C++ 语句,所以它们不会以分号(; )结尾。
//3.#define 预处理指令:#define 预处理指令用于[创建符号常量]。该符号常量通常称为宏
// (1)无参宏:#define name value,在该文件中后续出现的所有宏都将会在程序编译之前被替换为value
// (2)参数宏:#define name(参数) 运算表达式,替换函数表达
//注意:宏定义只做替换,不做运算。一个标识符被宏定义后,该标识符便是一个宏名。这时,在程序中出现的是宏名,在该程序被编译前,先将宏名用被定义的字符串替换,这称为宏替换,替换后才进行编译,宏替换是简单的替换。
#define PI acos(-1) //宏定义PI
#define MIN(a,b) (a<b?a:b)//宏定义函数
int main() {
cout << PI << endl;
cout << MIN(2, 3) << endl;
return 0;
}
2.条件编译指令
一.条件编译指令:条件编译是指预处理器根据条件编译指令,有条件地选择源程序代码中的一部分代码作为输出,送给编译器进行编译。主要是为了有选择性地执行相应操作,防止宏替换内容(如文件等)的重复包含。
1.内容:
(1)#if :编译预处理中的条件命令,相当于C语法中的if语句。如果条件为真,则执行相应操作(2)#ifdef :判断某个宏是否被定义,若已定义(不管有没有赋值),执行随后的语句
(3)#ifndef :与#ifdef相反,判断某个宏是否未被定义,若未被定义则执行随后的语句。
(4)#elif :若#if, #ifdef, #ifndef或前面的#elif条件不满足,则执行#elif之后的语句,相当于C语法中的else-if
(5)#else:与#if, #ifdef, #ifndef对应, 若这些条件不满足,则执行#else之后的语句,相当于C语法中的else
(6)#endif:#if, #ifdef, #ifndef这些条件命令的结束标志.
2.常用组合:
(1)#if——#else——#endif
(2)#ifndef——#define——#endif:该组合一般用于检测程序中是否已经定义了名字为某标识符的宏,如果没有定义该宏,则定义该宏,并选中从 #define 开始到 #endif 之间的程序段;如果已定义,则不再重复定义该符号,且相应程序段不被选中。该条件编译指令更重要的一个应用是防止头文件重复包含。
(3)#ifdef--#else--#endif
#include <iostream>
#include <cmath>
using namespace std;
#define PI acos(-1) //宏定义PI
#define MIN(a,b) (a<b?a:b)//宏定义函数
//条件编译指令:条件编译是指预处理器根据条件编译指令,有条件地选择源程序代码中的一部分代码作为输出,送给编译器进行编译。主要是为了有选择性地执行相应操作,防止宏替换内容(如文件等)的重复包含。
//(1)内容:
// #if 编译预处理中的条件命令,相当于C语法中的if语句。如果条件为真,则执行相应操作
// #ifdef 判断某个宏是否被定义,若已定义(不管有没有赋值),执行随后的语句
// #ifndef 与#ifdef相反,判断某个宏是否未被定义,若未被定义则执行随后的语句。
// #elif 若#if, #ifdef, #ifndef或前面的#elif条件不满足,则执行#elif之后的语句,相当于C语法中的else-if
// #else 与#if, #ifdef, #ifndef对应, 若这些条件不满足,则执行#else之后的语句,相当于C语法中的else
// #endif #if, #ifdef, #ifndef这些条件命令的结束标志.
//(2)常用组合:
// #if——#else——#endif
// #ifndef——#define——#endif
// 作用:一般用于检测程序中是否已经定义了名字为某标识符的宏,如果没有定义该宏,则定义该宏,并选中从 #define 开始到 #endif 之间的程序段;如果已定义,则不再重复定义该符号,且相应程序段不被选中。
// 该条件编译指令更重要的一个应用是防止头文件重复包含。
// #ifdef--#else--#endif
int main() {
#ifdef PI
cout << "Define PI" << endl;
#else
cout << "Define No PI" << endl;
#endif // PI
cout << "main end" << endl;
return 0;
}
3.# 和 ## 运算符
(1)#运算:# 运算符会把 replacement-text 令牌转换为用引号引起来的字符串。即 #x -> "x"
(2)##运算:## 运算符用于连接两个令牌参数。即 x##y -> xy
//5.# 和 ## 运算符
// (1)#运算:# 运算符会把 replacement-text 令牌转换为用引号引起来的字符串。即 #x -> "x"
// (2)##运算:## 运算符用于连接两个令牌参数。即 x##y -> xy
#define STR(x) #x //字符串转换"x"
#define CONCAT(x,y) x##y //拼接xy
int main() {
//#运算
cout << STR(HELLO WORLD!) << endl;
//##运算
int xy = 100;
cout << CONCAT(x, y) << endl;
return 0;
}
五.头文件与源文件
1.C++程序编译过程
(1)编译预处理(预编译)
在预编译阶段,读取源程序(.c/.cpp),对源程序进行宏替换,条件编译,头文件展开/嵌入,去除注释,生成处理后的文本文件(.i)
(2) 编译
在编译阶段,将上一步预处理后的文本文件(.i)进行语法分析,语义分析以及优化后产生汇编代码文件(.s),汇编代码文件仍是文本文件
(3)汇编
在汇编阶段,就是将编译出来的汇编代码文件(.s)翻译成机器指令的过程,生成可执行的二进制可重定位目标程序(Linux下为.o/Windows下为.obj)
(4)链接
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。所以链接阶段就是将所有相关文件链接到一起生成一个整体的可执行二进制程序(.out/.exe)
(5)注意
- 在 compiling 阶段,各编译单元还各自为政, 看不见其他源文件中的内容;在 linking 阶段,才会把各编译单元链接起来,让它们打照面,这也涉及到声明和定义的问题。
- 缺省情况下这个最终可执行文件的名字被命名为a.out。
2.#include 宏命令
1. 作用:#include 是一个宏命令,它在预编译阶段的时候就会起作用。#include 的作用是把它后面所写的那个文件的内容,完完整整地、一字不改地包含嵌入到当前的文件中来。
2. 包含内容:包含指令不仅仅限于.h头文件,可以包含任何编译器能识别的C/C++代码文件,包括.c、.hpp、.cpp、.hxx、.cxx等
3. 区别:.cpp文件是需要编译的文件,成为一个独立的编译单元,而.h文件从来是不需要编译,只是用于预处理声明。
4. 意义:include多用于引入头文件.h,include的一个重要意义就是在大规模工程项目中进行模块化、代码复用
5. 引入方式:
(1) include<> : #include <> 的查找位置是标准库头文件所在目录,先去系统目录中找头文件,如果没有在到当前项目目录下找,多用于引入系统库文件
(2) include"" : #include "" 的查找位置是当前源文件所在目录,先在当前目录下寻找,如果找不到,再到系统目录中寻找。多用于引入自定义头文件
3.声明和定义
1.声明和定义的区别:声明不一定会定义,但是定义了一定包含声明
(1)声明:声明只是告诉编译器存在某个类型的变量或者函数,但是编译阶段不分配任何内存空间,只有到链接阶段才会去找该声明的定义实现
- 变量声明:extern int a; //只是声明了有一个变量 a 存在,具体 a 在哪定义的,需要到时候去找。(无初值,无内存)
- 函数声明:int function();或extern int function(); //只有函数头的就是声明
- 类声明: class B;
- 对象声明: extern B temp;
(2)定义:分配空间,并给出具体实现
- 变量定义:int a;或int a = 1;//定义变量a,分配内存空间并给出初值(前者随机赋值)
- 函数定义:int function(){};//只要有函数体就是函数定义
- 类定义: class B{}
- 对象定义:B b()或B *b = new B()
(3)注意:
- 在一个项目程序中只能对变量或函数定义一次,但是可以多次声明。因为我们不能让编译器一直为同一个变量或函数分配不同的存储空间;
- 同一个定义在不同文件分别编译运行时是可行的,他们属于独立的整体。但是在同一个项目下(只有一个main函数),所有的源文件最终会被链接为一个整体文件执行,多次定义就会报错(链接阶段报错)
- 这也是为什么,我们强调在头文件中只能存放变量或函数的声明,而不要进行定义。因为一个头文件会在多处被引用
- 这样一来,按道理说,一个文件中定义的全局变量,可以在整个程序的任何地方被使用,举例说,如果A文件中定义了某全局变量,那么B文件中应可以直接使用该变量。但是在B中要提前声明extern int a;因为在链接之前我们要先通过编译阶段。编译的要求是先声明才能使用!
//code/a.cpp
//定义全局变量 temp
int temp = 2;
//定义函数
int display() {
return 6;
}
//code/b.cpp
#include<iostream>
using namespace std;
//声明变量temp,链接时寻找定义实现
extern int temp;
//声明函数display()
int display();
int main() {
cout << temp << endl;
cout << display() << endl;
return 0;
}
4.头文件 .h
1.头文件 .h
(1)定义:头文件其实跟 .cpp 文件中的内容是一样的,都是 C++ 的源代码。但头文件不用被编译。
(2)为什么要头文件:在大规模项目中,要实现模块化和复用,我们希望把重复代码放到一个文件里,在别处引用。但是这有可能会导致多次定义的情况发生,因此我们可以使用头文件,在头文件中放置变量和函数的声明来达到一次定义,多处使用,还能通过编译的情况。
(3)头文件中应该写什么:
- 头文件只能存在变量或者函数的声明,而不要放定义。
- 头文件中可以写 const 对象的定义。
- 头文件中可以写内联函数(inline)的定义。
- 头文件中可以写类(class)的定义。一般,我们的做法是,把类的定义放在头文件中,而把函数成员的实现代码放在一个 .cpp 文件中。但是如果函数成员在类的定义体中被定义,那么编译器会视这个函数为内联的,是合法的。
- static 普通变量和普通函数的定义,但是不能包含,static 成员函数和成员变量的定义。
(4)头文件中的保护措施
- 问题:如果 a.h 中含有类 A 的定义,b.h 中含有类 B 的定义,由于类B的定义依赖了类 A,所以 b.h 中也 #include了a.h。现在有一个源文件,它同时用到了类A和类B,于是程序员在这个源文件中既把 a.h 包含进来了,也把 b.h 包含进来了。这时,问题就来了:类A的定义在这个源文件中出现了两次!于是整个程序就不能通过编译了。
- 解决:#ifndef XXXXXXXXXXX_H_
#define XXXXXXXXXXX_H_
头文件内容(防止重复包含定义)
#endif