C++学习之路

C++基础,纯手打

一、理论知识

gcc 编译步骤

hello.c

  1. 预处理g++ -E :展开宏,头文件,替换条件编译,删除注释,空行,空白
  2. 编译g++ -S:检查语法规范
  3. 汇编g++ -c:将汇编指令翻译成机器指令
  4. 链接:数据段合并,地址回填

直接g++ -c
gcc命令参数
-I: 指定头文件的目录
-g: 编译带调试信息的可执行文件
-O: 优化源代码(编译慢,文件小,运行效率高)
-l/-L:指定库文件/指定库文件路径
-Wall:打印警告信息
-w:关闭警告信息
-std=c++11:设置编译标准
-o:指定输出文件名
-D:定义宏

静态库/动态库(shared库)

静态库:可执行程序运行钱就已经加载到执行代码中。成为执行代码的一部分
动态库: 在执行程序启动时加载到执行程序中,可以被多个程序共享使用(建议使用)
静态库制作及使用:

ar rcs libmyl ib.a file.o:以lib开头 .o结尾

  1. 将.cpp生成.o文件:g++ -c xxx.cpp xxx1.o
  2. 使用ar制作静态库:ar rcs libxxx.a xxx1.o xxx2.o
  3. 编译静态库到可执行文件中:g++ new.cpp libxxx.a -o a.out

动态库制作及使用:

  1. 将.cpp生成.o文件(生成与位置无关的代码 -fPIC):g++ -c xxx.cpp -o xxx.o -fPIC
  2. 使用g++ -shared制作动态库:g++ -shared -o libxxx.so xxx1.o xxx2.o
  3. 编译可执行程序时,指定所使用的动态库(去掉lib前缀:libmylib.so->-lmylib):g++ test.c -o a.out -lmylib -L./lib
  4. 运行a.out出错:需要指定动态链接库(通过设置环境变量LD_LIBRARY_PATH=动态链接库地址或者配置/etc/ld.so.conf文件)

通过ldd a.out查看调用的动态链接库

GDB调试工具

Makefile及Cmake

CMake

基本语法特征

基础语法格:指令(参数1 参数2)

参数使用括弧

参数之间使用空格活分号分开

指令是大小写无关的,参数和变量是大小写相关的**

1
2
3
set(HELLO hello.cpp)
add_executable(hello main.cpp hello.cpp)
ADD_EXECUTABLE(hello main.cpp ${HELLO})

变量使用${}方式取值,但是在IF控制语句中是直接使用变量名

重要指令和常用变量

cmake_minmum_required:指定CMake的最小版本要求

1
2
3
# cmake_minmum_required(VERSION versionnumber [FATAL_ERROR])
# 指定最下版本要求为2.8.3
cmake_minmum_required(VERSION 2.8.3)

project:定义工程名称,并可指定工程支持的语言

1
2
3
# project(projectname [CXX][C][Java])
# 指定工程名为HELLOWORLD
project(HELLWORLD)

set:显式的定义变量

1
2
3
# set(VAR [VALUE][CACHE TYPE DOCSTRING[FORCE]])
# 定义SRC变量的值为main.cpp hello.cpp
set(SRC sauhello.cpp hello.cpp)

include_directories:向工程添加特定的头文件搜索路径

1
2
3
# include_directories([AFTER|BEFORE][SYSTEM]dir1 dir2...)
# 将/usr/include/myincludefolder 和 ./include添加到搜索路径
include_directories(/usr/include/myincludefolder ./include)

link_directories:向工程添加特定的库文件搜索路径

1
2
3
# link_directories(dir1 dir2...)
# 将/usr/lib/mylibfolder 和 ./lib添加到库文件搜索路径
link_directories(/usr/lib/mylibfolder ./lib)

add_library:生成库文件

1
2
3
# add_library(libname[SHARED|STATIC|MODULE][EXCLUDE_FROM_ALL] source1 source2...)
# 通过变量 SRC 生成libhello.so共享库
add_library(hello SHARED ${SRC})

add_compile_options:添加编译参数

1
2
3
# add_compile_options(<option>)
# 添加编译参数 -Wall -std=c++11 -o2
add_compile_options(-Wall -std=c++11 -o2)

add_execuable:生成可执行文件

1
2
3
# add_execuable(exename source1 source2 ...)
# 编译main.cpp生成可执行文件main
add_execuable(main main.cpp)

taget_link_libraries:为target添加需要链接的共享库

1
2
3
# taget_link_libraries(target library1<debug|optimized> library2 ...)
# 将hello动态库文件连接到可执行文件main中
taget_link_libraries(main hello)

add_subdirectory:向当前工程中添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置

1
2
3
# add_subdirectory(source_dir [binary_dir][EXCLUDE_FROM_ALL])
# 添加src子目录,sor中需有一个CMakelsits.txt
add_subdirectory(src)

add_source_directory:发现一个目录下所有的源代码文件并将列表存储在一个变量中,这个指令临时被用来自动构建源文件列表

1
2
3
4
5
# add_source_directory(dir VARIABLE)
# 定义SRC变量,其值为当前目录下的有所源代码文件
add_source_directory(. SRC)
# 编译SRC变量所代表的源代码文件,生成main可执行文件
add_executable(main ${SRC})

常用变量:

CMAKE_C_FLAGS: gcc编译选项
CMAKE_CXX_FLAGS: g++编译选项

1
2
# 在CMAKE_CXX_FLAGES编译选项后最佳-std=c++11
set(CMAKE_CXX_FLAGS "${CAKE_CXX_FLAGS} -std=c++11")

CMAKE_BUILD_TYPE: 编译类型(Debug,Release)

1
2
3
4
# 设定编译类型为debug,调试时需要选择debug
set(CMAKE_BUILD_TYOE Debug)
# 设定编译类型为release,发布时需要选择release
set(CMAKE_BUILD_TYPE Release)
1
2
3
CMAKE_SOURCE_DIR  
PROJECT_SOURCE_DIR
<projectname>_SOURCE_DIR
  1. 三个变量内容一致。都是工程顶层目录
  2. zaiin source build时,跟CMAKE_BINARY_DIR等变量一直
  3. PROJECT——SOURCE——DIR 略有不同。可这样理解

CMAKE_C_COMPILER:指定编译器
CMAKE_CXX_COMPILER:指定C++编译器
EXECUTABLE_OUTPUT_PATH:可执行文件输出的存放路径
LIBRARY_OUTPUT_PATH:库文件输出的存放路径

使用CMake编译工程

项目主目录存在一个CMakeKists.txt

两种编译规则:

  1. 包含源文件的子文件夹包含CmakeLsit.txt,主目录的CmakeLists.txt通过add_subdirectory添加子目录即可;
  2. 包含源文件的子文件夹未包含CMakeLists.txt,子目录便以规则体现在主目录的CmakeLists.txt中

流程:

  1. 编写CmakeLists.txt。
  2. 执行命令cmake PATH生成Makefile(PATH是顶层CmakeList.txt所在的目录)。
  3. 执行命令make进行编译。

构建方式:

  1. 内部构建:与源文件同级
  2. 外部构建:不同的目录(推荐)

例:使用最简单的CMake编译HelloWorld

hello.cpp:

在.cpp同级目录创建CMakeLists.txt:

1
2
3
4
5
cmake_minimum_required(VERSION 3.0)

project(HELLO)

add_executable(hello_cmake hello.cpp)

执行 :cmake .:
控制台信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
goth@goth-ubuntu:~/Project/CPPwork/testHello$ cmake .
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/goth/Project/CPPwork/testHello

执行make编译代码:

1
2
3
4
5
goth@goth-ubuntu:~/Project/CPPwork/testHello$ make
Scanning dependencies of target hello_cmake
[ 50%] Building CXX object CMakeFiles/hello_cmake.dir/hello.cpp.o
[100%] Linking CXX executable hello_cmake
[100%] Built target hello_cmake

结果:
运行结果

外部构建:
bash:

1
2
3
4
mkdir build
cd build

cmake ../
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
goth@goth-ubuntu:~/Project/CPPwork/testHello/build$ cmake ../
-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/goth/Project/CPPwork/testHello/build

bash:
make:

1
2
3
4
Scanning dependencies of target hello_cmake
[ 50%] Building CXX object CMakeFiles/hello_cmake.dir/hello.cpp.o
[100%] Linking CXX executable hello_cmake
[100%] Built target hello_cmake

生成文件目录:

20210713210914

编译略复杂项目:
结构如下:

1
2
3
4
5
6
7
8
9
10
goth@goth-ubuntu:~/Project/CPPwork/test$ tree
.
├── include
│ └── swap.h
├── lib
├── main.cpp
└── src
└── swap.cpp

3 directories, 3 files

结构如下:

二、C++基础

命名空间

代码:

1
2
3
4
5
6
7
8
9
10
11
namespace A{
void func();
int a = 1;
struct Persion{

};
class Animal{};
namespace B{
int b = 10
}
}

1.命名空间下可以存放:函数、变量、结构体、类
2.命名空间必须定义再全局作用域下
3.命名空间可以嵌套命名空间
4.命名空间是开放的,可以随时添加内容,相同命名空间名称会进行合并
5.无名/匿名空间:相当于 static int c;static int d;

1
2
3
4
namespace{
int c = 0;
int d= 0;
}

6.命名空间可以取别名

1
2
3
4
void test04(){
namespace newA = A;
cout<<newA::a<<endl;
}

using关键字

using 声明注意避免二义性;

1
2
3
int a = 10;
using A::a;
cout<<a<<endl;//错误

using编译指令

1
2
using namespace A;
cout<<a<<endl;//正常
1
2
3
int a = 10;
using namespace A;//打开空间,并没有声明。准循就近原则
cout<<a<<endl;//正常
1
2
3
4
5
int a = 10;
using namespace A;//打开空间,并没有声明。准循就近原则
using namespace C;//C存在变量a

cout<<a<<endl;//错误

C++对C的增强

1.全局变量检测增强

1
2
int a;
int a = 10;//正常
1
2
int a;
int a = 10;//重定义异常

2.参数类型增强,函数检测增强,返回值检测增强,函数调用参数检测异常

C:

1
2
3
int getNum(w,h){

}//正常

C++:

1
2
3
int getNum(w,h){

}//异常:需要确定w,h的类型
1
2
3
int getNum(int w, int h){

}//异常没有返回值
1
2
3
int getNum(int w, int h){
return w*h;
}//正常

C:

1
2
3
void test(){
getNum(10,10,12);//正常
}

C++:

1
2
3
void test(){
getNum(10,10,12);//调用参数检测异常
}

3 类型转换检测增强

1
2
3
void test(){
char *p = malloc(sizeof(64));//malloc返回值为void*
}
1
2
3
4
void test(){
char *p = malloc(sizeof(64));//错误:malloc返回值为void*
char *p = (char*)malloc(sizeof(64));//正确
}

4 struct 增强

C:

1
2
3
4
struct Person{
int age;
void addAge();//错误:c语言中struct不可以加函数
}

C++

1
2
3
4
5
6
7
struct Person{
int age;
void addAge();//正确
void addAge2(){
age++;
};//正确
}
1
2
Persion p1//错误,必须加struct关键字
struct Persion p2//正确
1
2
Persion p1//正确,可不加struct关键字
struct Persion p2//正确

5 bool类型增强(C中没有bool类型)

bool 类型 非0转为1

6 三目运算符增强

c++返回的是变量
c返回的是

C:

1
2
(a>b?a:b)=100;//错误:a=10,b=20则含义为 20=100
*(a>b?&a:&b)=100;//正确:*取数据所在地址的值

C++

1
(a>b?a:b)=100//正确

7 const增强

  1. C语言中的局部变量const是伪常量,编译器会分配内存
    C++中,const不会分配内存:const int b = 20;
    C++中使用地址强改会开辟新的内存,不会影响const的值
  2. C语言中 const 默认外部链接
    C++中的const默认内部链接,只能在当前文件中使用
    若想使用外部链接定义:
    extern const int a = 10
    调用:
    extern const int a

const内存情况:

分配内存都可以绕过编译器,进行修改

  1. const基本数据类型不分配内存 取地址会分配临时内存
  2. extern编译器也会给consat变量分配内存
  3. 用变量初始化const变量,会分配内存
    int a = 10
    const int b = a
  4. 自定义数据类型 加 const也会分配内存

尽量使用const代替#define

引用

引用的语法与注意事项

&=号左侧为引用。=号右侧为取地址。

1 引用语法:Type &别名 = 原名
2 引用必须初始化,引用初始化后不可以修改只能赋值
3 引用必须引一块合法的内存空间
4 不要返回局部变量的引用,除非局部变量为静态,如果函数的返回值为引用,那么这个函数调用可以为左值

1
2
3
4
5
6
7
8
9
>int& doWork(){
int a = 10;
//static int a = 10;//正确
return a;
>}

>int &ret = doWork();//上函数局部变量a已经被销毁啦,第一次使用正常。第二次使用将错误。

>doWork() = 1000;//返回值为引用,调用为左值。a=1000;

5 对数组建立引用

1
2
3
4
5
6
7
8
9
>//方法1:
>int arr[10];
>for (int i = 0;i<10;i++){
arr[i] = i;
>}
>int (&parr)[10] = arr;//小括号不可省略
>//方法2:
>typedef int(ARRAYREF)[10];//一个具有10个元素的int类型的数组
>ARRAYREF &pArr2 = arr;

引用的本质

引用的本质是指针常量

参数的传递方式

值传递

略:原值无变化

地址传递

1
2
3
4
5
6
7
8
9
10
void mySwap(int *a,int *b) {
int temp = *a;
*a = *b;
*b = temp;
}

int a = 1;
int b = 2;
mySwap(&a, &b);
//cout a,b的值发生改变

引用传递

//等价于地址传递,只是将原位置起个新名字传递过去

1
2
3
4
5
6
7
8
9
10
void mySwap(int &a,int &b) {
int temp = a;
a = b;
b = temp;
}

int a = 1;
int b = 2;
mySwap(a, b);
//cout a,b的值发生改变

指针的引用

用一级指针引用代替二级指针

传统指针方式:(难理解)

1
2
3
4
5
6
7
8
9
10
11
12
13
//定义结构体
struct Person{
int m_Age;
};
//给p分配内存
void allocatMemory(Oerson **p){//**p 具体的Person对象、*p 对象的指针 p指针的指针
*p = (PerSon *)malloc(sizeof(Person));
(*p)->m_Age = 100;//指针操作用->
}

Person *p = NULL;
allocaMemory(&p);
cout<<"p的年龄:"<<p->m_Age<<endl;

利用指针以用开辟空间

1
2
3
4
5
6
7
8
9
10
11
12
13
//定义结构体
struct Person{
int m_Age;
};
//给p分配内存
void allocatMemory(Person* &p){
p = (PerSon *)malloc(sizeof(Person));
(p)->m_Age = 100;//指针操作用->
}

Person *p = NULL;
allocaMemory(p);
cout<<"p的年龄:"<<p->m_Age<<endl;

常量引用

使用场景:修饰形参为只读
const int a = 10:会分配内存

1
2
3
4
5
6
7
//int &ref = 10;//引用了不合法的内存,不可以
const int &ref = 10;//加入const后,编译器处理方式为:int tmp = 10;const int &ref = tmp;

//ref=10//不可以直接修改

int *p = (int*)&ref;//强制修改
//ref=1000;
1
2
3
4
5
6
7
//以用使用场景 用来修饰形参
void showValue(const int &val){
//如果只是想显示内容,而不修改内容,那么就用const修饰这个形参
cout<<"val="<<val<<endl;
}
int a = 10;
showValue(a);

const 引用场景;

常量引用主要用在函数的形参,尤其是类的拷贝/复制构造函数
优势:

  1. 引用不产生新的变量,减少形参与实参传递的开销
  2. 由于引用可能导致实参随形参改变而改变,将其定义为常量引用可以消除这种副作用。希望实参随着形参改变使用一般引用。不希望改变则使用常引用。

系统会提供3个默认函数:默认构造拷贝构造析构函数

设计抽象类

1
2
3
4
5
class 类名{
public:
设置成员属性
设置成员函数
}

使用:

类名 对象名;

对象操作

分文件编写

.h写类成员函数的声明
.cpp写成员函数的实现

Point.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#pragma once
#include <iostream>
class Point
{
public:
void setXY(int x, int y);
int getX();
int getY();

private:
int x;
int y;
};

Point.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "Point.h"
void Point::setXY(int x, int y) {
this->x = x;
this->y = y;
}

int Point::getX() {
return x;
}
int Point::getY() {
return y;
}

初始化和清理

初始化:构造函数
清理:析构函数

1
2
//未提供构造函数
Person p1;//系统默认构造和析构,由系统提供

创建Person类,并提供构造函数

分类:

按照参数类型进行分类:无参构造函数有参构造函数
按照类型分类:普通构造函数拷贝函数

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
#pragma once
#include <string>
using namespace std;
class Person
{
public:
//构造函数写法
//与类名相同,没有类型,可重载
//构造函数由编译器自动调用,而不是手动,而且只会调用一次
Person();
Person(int id,int age,string name,bool gender);
//拷贝构造函数
//可以复制存在的Person类。必须加const关键字
//
Person(const Person& p);
//析构函数写法
//与类型相同类名。没有返回类型,无参(无法重载)
//自动调用,函数结束时调用一次
~Person();

private:
int id;
int age;
string name;
bool gender;
};

构造函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//括号法调用
Person p1(1,21,"小红",0);//有参
Person p2(p1);//拷贝
Person p3//默认,切记不能加()
//Person p3();//函数声明,加了()就是默认构造函数的函数声明

//显示法调用
//Person(2,22,"小刚",1),为匿名对象。并起名我p4l
person p4 = Person(2,22,"小刚",1);
person p5 = Person(p4);

//匿名对象
Person(2,22,"小刚",1);//改行代码运行完直接调用析构函数
//Person(p5)//错误:无法使用拷贝构造函数初始化匿名对象
//编译器认为该句为 Person p5(声明),重定义异常
Person p6 = person(p5);//写右值可以


Person p7 = 100;//相当于调用Person p7 = Person(100),隐式类型转换
Person p8 = p7;//相当于Person p8 =Person(p7);

拷贝函数调用的方式

1 用已经创建好的对象初始化新的对象

1
2
Person p1(1);
Person p2(p1);

2 以值传递的方式给函数参数传值

1
2
3
4
void doWork(Person p1){//Person p1 = Person(p);//使用了拷贝构造
}
Person p(1);
doWork(p);

3 以值方式返回局部对象

1
2
3
4
5
6
Person doWork(){
Person p1;
return p1;
}

Person p =doWork();

构造函数的调用规则

  1. 当我们提供了有参构造函数,系统不会再提供默认构造函数,需要重写无参构造函数,仍然提供默认拷贝构造函数

  2. 当我们提供了拷贝构造函数,系统不会再提供其他构造析构函数依然提供

深拷贝和浅拷贝

浅拷贝(系统默认拷贝):拷贝值

如果属性里有指向堆区空间的数据,那么简单的浅拷贝会导致重复释放内存的异常

深拷贝:拷贝数据

初始化列表的基本使用

1
2
3
Person(int a,int b,int c):A(a),C(c){

}

等同于:

1
2
3
4
Person(int a,int b,int c){
A=a;
C=c;
}

默认参数初始列表

1
2
3
4
Person():A(10),B(20),C(30){

}

类作为类成员的案例

visualstudio:”类对象作为类成员”

类对象作为类成员时,构造顺序先将类对象–构造。然后构造自己,析构顺序相反

explicit关键字

防止隐式类型转换

new运算符 动态对象创建

malloc函数的问题:

  1. 程序员必须确定对象的长度。
  2. malloc返回的void指针.C++不允许将void赋值给其他任何指针,必须强转。
  3. malloc可能申请失败,所以必须判断返回值来确保内存分配成功
  4. 用户使用对象之前必须记住对他初始化,构造函数不能显示调用初始化(构造函数由编译器调用),用户可能忘记调用初始化函数。

new使用:

1
2
3
Animal a1;//栈区开辟空间
Animal *a2 = new Animal;//堆区开辟空间
delete a2;

所有new出来的对象,都会返回该类型的指针。malloc返回void*还要强转
new 会调用构造函数,malloc不会
delete释放堆区空间

void *接受指针导致无法delete

1
2
void *a = new Animal;//无法
delete a;//不能调用析构函数

通过new开辟数组,一定会调用默认构造函数所以一定要提供默认构造

1
2
3
4
5
Animal *aArray = new Person[10];//构造函数调用10次

Animal pArray[2] = {Person(1),Person(2)};//栈上开辟数组,可以指定有参构造

delete [] aArray;//释放数组一定要加中括号,否则只会释放数组中的一个

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

静态成员变量
  1. 编译阶段分配内存
  2. 所有对象共享数据
  3. 对象访问,类名访问
  4. 具有权限控制
  5. 类内声明,类外初始化
  6. 可以被普通方法访问

静态成员变量需要类内声明,类外初始化实现。这样便可以使用类名访问其静态属性。私有权限无法访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
public:
Person();
static string m_name;

private:
static int m_Age;
};
string Person::m_name = "小明";//类外初始化实现
int Person::m_Age = 0;//类外初始化实现私有


void test(){
cout<<Person::m_name<<endl;//无需实例化可直接访问
//cout<<Person::m_Age<<endl;//私有权限无法访问
}
静态成员函数
  1. 对象访问,类名访问
  2. 具有权限控制
  3. 可以被普通方法访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
```C++
class Person {
public:
Person();
static string m_name;

static void fun1(){
cout<<"静态成员函数"<<endl;
};
private:
static int m_Age;
static void fun2(){
cout<<"静态成员函数"<<endl;
};
};

void test(){
fun1();
//Person::fun2();//没有访问权限
}

静态成员变量实现单例模式

目的:为了让类只有一个实例,实例不需要自己释放

  1. 默认构造拷贝构造私有化
  2. 内部类维护一个对象指针
  3. 私有化唯一指针
  4. 对外提供getinstance方法来访问这个指针

例1:主席单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ChairMan {
private:
//私有化构造函数和拷贝构造
ChairMan() {}
ChairMan(const ChairMan &c) {}
//静态实例类内声明
static ChairMan *singleMan;
public:
//创建getInstance函数获得当前单例
static ChairMan* getInstance() {
return singleMan;
}

};
//静态类类外创建
ChairMan *ChairMan::singleMan = new ChairMan;

int main()
{
ChairMan * cm1 = ChairMan::getInstance();//获得实例
}

封装

C语言属性和行为分开处理啦,类型检测不够
C++中封装严格类型转换检测,让属性和行为绑定在一起。
C++ 有权限控制

structclass 一个意思,唯一的不同是默认权限不同。
struct:默认权限public
class:默认权限private

C++中的权限:

private
protected
public

与java一样建议将内部属性设置为私有,提供get,set方法访问。

对象模型初探

  1. 空类大小为1,用char维护对象的地址
  2. 成员变量和成员函数是分开存储
  3. 非静态成员变量属于对象上。静态成员变量方法和非静态成员方法都不属于对象

this指针的使用

  1. 永远指向当前对象
  2. this解决命名冲突
  3. *this指向对象的本体
  4. 非静态成员函数才有this指针,可以加if判断,如果this为NULL就return

空指针访问成员函数

  1. 如果成员函数没有用到this,那么空指针可以直接访问
  2. 如果成员函数用的this指针使用到

常函数与常对象

常函数: void func() const {}
不允许修改指针指向的值

mutable关键字:修改的值常函数可以修改

常对象:不允许修改属性

  1. 不能调用普通函数,可以调用常函数
  2. 只能读不能修改属性。

const Person p1;

友元

全局函数作友元函数
  1. 友元关系不能被继承
  2. 友元关系是单向的,类A是B的友元,B不一定是A的友元
  3. 友元关系不具有传递性
类作友元类

目的:访问类中的私有成员属性
friend goodGay::visit();

成员函数做友元函数

目的:访问类中的私有成员属性
friend class A

函数

内联函数(inline function)

作用:用来代替C语言的宏函数

宏函数:

#define MyAdd(x,y)((x)+(y)):问题多,没有作用域

使用:

1
inline void func(int a);//声明,无任何效果

写法:

1
inline int func(int a){return a++;}
类内部的内联函数

类内部的内联函数不是必须要求inline关键字。类内部定义的成员函数默认加inline自动成为内联函数

内联函数的限制

inline关键字只是建议编译器,编译器也可能对非内联函数内联。编译器在以下条件下可能不会对函数进行内联编译:

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

函数的默认参数

C++在声明函数原型的时可指定缺省值。

1
2
3
int sum(int a,int b, int c=10){
rerutn a+b+c;
}
  1. 如果默认参数在参数列表最后面,也就是说,每个有默认参数的参数后面的参数都有默认值
  2. 如果函数在声明时有了默认参数,那么函数实现时必须没有

函数的占位参数

没用但是不能没有

1
2
3
void fun(int a,int){

}

可以有默认值

1
2
3
void fun(int a,int = 1){

}

函数重载

与java类似但是返回值不能作为重载的条件!!!
在同一作用域下,函数名相同。参数列表(类型,个数,顺序)不同。(返回值不能作为条件)

函数重载碰到默认参数,要避免二义性。
例子:

1
2
3
4
5
6
7
8
void fun(int a,int b =1){

}
void fun(int a){

}

fun()//调用

引必须要引合法的空间内存
const 耶可以作为重载条件

例子:

1
2
3
4
5
6
7
8
void fun(int &a){//不能直接调用基本数据类型

}
void fun(const int &a){//可以调用

}

fun(10)

原理: 编译器会根据不同参数编译成不同函数名

extern “C”浅析

使用C编译改代码
比如C语言实现show()方法:

s.h

1
void show();

s.c

1
2
3
void show(){
print("Hello World");
}

在C++中调用该方法有错误。因为C++函数存在重载,会改名字。

因此在C++中使用:
去掉头文件

1
extern "C" void show();//按照C语言的方法做链接

当c代码太多使用如下方法:
在C代码中:

1
2
3
4
5
6
7
8
9
10
#pragma once
#ifdef __cplusplus//两个下划线
extren "C"{
#endif
#include <stdio.h>
void show();

#ifdef __cplusplus
}
#endif

运算符重载

不要乱用符号重载
内置数据类型的运算符不能重载

operator[+][-][*][/]

如重载+运算符

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

//成员函数:重载
Person operator+ (Person &p){
Person tem;

tem.a = this->a+p.a;
tem.b = this->b+p.b;
return tem;
}

//全局函数:重载
Person operator+ (Person &p1,Person &p2){
Person tem;
tem.a = p1.a+p2.a;
tem.b = p1.b+p2.b;
return tem;
}
//全局函数:重载
Person operator+ (Person &p1,int a){
Person tem;
tem.a = p1.a+a;
tem.b = p1.b+a;
return tem;
}

//调用:
Person p1 = (1,2);
Person p2 = (3,4);

Person p3 = p1+p2;
Person p4 = p1+10;

//p3.a = 4;
//p3.b = 6;

重载左移运算符

重载左移运算符不能写在成员函数中

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
class Person{
friend ostream& operator << (ostream &cout, Person &p1);//访问私有函数,将其设为友元函数
public:
Person() {}
Person(int a,int b) {
this->m_A = a;
this->m_B = b;
}

/*不能写在类里,不是此类调用
*ostream& operator << (ostream &cout,Person &p1) {
*cout <<p1.m_A<<p1.m_B<< endl;
*return cout;
*}
*/

private:
int m_A;
int m_B;
};
ostream& operator << (ostream &cout,Person &p1) {
//cout为ostream类型。为了能继续输出endl,返回值为ostream cout的引用;
cout <<p1.m_A<<p1.m_B<< endl;
return cout;
}
int main()
{
Person p1(10, 10);
cout << p1 << endl;;
}

递增运算符重载

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
class MyInterger{
public:
MyInterger() {
m_Num = 0;
};

//前置++
MyInterger& operator ++() {
//返回引用
this->m_Num =m_Num+ 1;
return *this;
}
//后置++
MyInterger operator ++(int) {
//返回值
MyInterger tem = *this;
this->m_Num = m_Num + 1;
return tem;
}

int m_Num;
};

ostream& operator <<(ostream &out,MyInterger inter) {
cout<<inter.m_Num<<endl;
return cout;
}

int main()
{
MyInterger myInt;
std::cout << myInt++;

std::cout << myInt;
}

指针运算符重载

智能指针:用来托管自定义类型的对象,让对象自动释放

赋值运算符重载

索引运算符重载

关系运算符重载

函数调用运算符重载

() 仿函数

继承

1
class Son : [public] Base{};
  1. 基类中的private 子类都不可以继承
  2. public 继承 ,原public/protect不会改变
  3. protect 继承,public/protect都变为protect
  4. private 继承,public/protect都变为private

继承会继承私有成员,只是被编译器隐藏

1
2
3
4
5
6
class Son : public Base{
public:
Son(int a):Base(a){
//利用初始换列表,显示调用有参构造
}
};

子类创建对象,先调用父类构造,然后调用自身构造
析构顺序相反
子类不会继承父类的构造函数和析构函数

继承中的同名处理

成员属性:直接调用,先调用子类。调用父类需要作用域
成员函数:直接调用先调用子类,父类的所有同名函数都会被隐藏(参数列表无关),除非用作用域

继承中的静态成员处理

成员属性:直接调用,先调用子类。调用父类需要作用域
成员函数:直接调用先调用子类,父类的所有同名函数都会被隐藏(参数列表无关),除非用作用域

多继承

1
class Son : [public] Base,[public] Base2{};

通过作用域解决二义性

菱形继承

同时拥有两个父类的属性

解决:

利用virtual虚基类

1
2
3
4
5
6
7
8
//虚基类 
son1: virtual public Base{
};
son2: virtual public Base{
};
sun : public son1,public son2{

};

sun1内部结构

  1. vbptr 虚基类指针
  2. 指向一张虚基类表
  3. 通过表找到偏移量

多态

定义:父类的引用和指针指向子类

静态联编和动态联编

静态联编:地址早绑定,编译阶段绑定好地址
动态联编:地址晚绑定。运行阶段绑定地址

使用virtual关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//虚函数
class A
{
public:
virtual void foo()
{
cout<<"A::foo() is called"<<endl;
}
};
class B:public A
{
public:
void foo()
{
cout<<"B::foo() is called"<<endl;
}
};
1
2
//纯虚函数
virtual void funtion1()=0

一个类使用了一个纯虚函数就是抽象类,无法实例化。子类必须实力化所有纯虚函数

虚析构

父类对象指向子类指针,父类的析构函数。可能对子类数据析构不干净。因此使用虚析构

1
virtual ~类名(){};

纯虚析构

父类对象指向子类指针,父类的析构函数。可能对子类数据析构不干净。因此使用虚析构。需要类内生命,类外实现

1
2
3
4
5
6
7
8
9
10
//在父类中
class 类名{
//类内声明
virtual ~类名() = 0;
}

类名::~类名(){
//类外实现
}

向下转型向上转型

向下转型:
基类转派生类:不安全

向上转型:
派生类转基类:安全

如果发生多态。向上转型向下转型总是安全的

C++模板

函数模板

普通函数可以进行饮食类型转换,隐式函数不可以隐式类型转换
如果出现重载,优先使用普通函数
如果想强制使用模板,可以使用空参数列表”<>例:swap<>(a,b)
如果模板函数更好匹配(如果同名普通函数会产生隐式类型转换),则优先使用模板函数

机制:

模板不是万能的,不能通用所有的数据类型
模板不能直接调用,需要生成模板函数
会对T替换后的类型进行二次编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <class T>//告诉编译器,T是通用类型
//template <typename T>//与class用法一样
void mySwap(T &a,T &b){
T tem = a;
a = b;
b = tmp;
}

int main(){
int i1 = 1;
int i2 = 2;
char c1 = 'a';
char c2 = 'b';
//1.自动推到类型
mySwap(a1,a2);
mySwap(c1,c2);
//mySwap(a1,c2);//无法推导,错误
//2.显示指定类型
mySwap<int>(a1,a2);

}

函数模板的局限性:

用户自定义类型无法直接进行数值比较。

通过具体化自定义类型,解决上述问题。

优先匹配具体化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class T>
bool myCompare(T &a,T& b){
if(a==b){
return true;
}
retunrn false;
}

template<> bool myCompare<>(Person &a,Person &b){//具体化自定义类型
if(a.m_Age = b.m_Age){
retun true;
}
return false;
}

类模板

类模板不支持自动类型推导,有默认类型参数

1
2
//Person p("小明",20);//不支持自动类型推导
Person<string,int> p("小明",20);

查看类型名称:typeid(T).name()

继承中的类模板

  1. 如果基类是模板类,必须让子类告诉编译器,基类中的T到底是什么类型,否则无法分配内存,编译不通过。class Child :public Base<int>

类模板的类外实现:

1
2
3
4
template <class T1,class T2>
Person<T1,T2>::Person(T1 name,T2 age){

}

类模板分文件编写(模板建议不要分文件编写):

将声明和实现写在一个类中。最后把后缀名改为.hpp

类模板与友元函数

类内实现:

1
2
3
friend void printPerson(Person<T1,T2> &p){

}

类外实现:

1
2
3
4
5
6
7
//提前让编译器看到类和方法
template<class T1,class T2> class Person;
template<class T1,class T2> void printPerson(Person<T1,T2>&p);


//加上<>模板函数声明
friend void printPerson<>(Person<T1,T2> &p);

类型转换

静态转换

static_cast关键字

  1. 可以对基本类型转换
  2. 可以对父子类型转换
  3. 不可以对无关类型转换
1
2
3
4
5
6
Base *base = NULL;
Child *child = NULL;
//不安全
Child *child2 = static_cast<Chile*>(base)
//安全
Base *base2 = static_cast<Base*>(base)

动态转换

dynamic_cast关键字

  1. 无法转换基础数据类型
  2. 普通情况子类无法转父类
  3. 多态子类父类都可以使用动态转换

常量转换

const_cast

  1. 不能对非指针或者非引用进行转换

重新解释转换(不安全,不用)

reinterpret_cast

异常

1
2
3
4
5
6
7
8
9
10
try{
//事件
throw 1
}
catch(int){
throw;//继续抛出
}
catch(...){//其他类型

}

栈解旋

从try开始到throw结束,所有栈上的对象都会被释放

异常的接口声明

//只抛出特定的异常

1
2
3
void func() throw (int,double){
throw 1;
}

异常的生命周期

  1. 如果使用MyException e:会调用拷贝构造,多开销一份数据
  2. 如果使用MyException *e:没有使用new会提前释放对象,使用new需要自己管理delete
  3. 推荐使用 MyException &e:同一份数据

异常的多态使用

使用同一个接口打印不同的异常音系

使用系统的标准异常

使用:略

编写自己的异常类,需要继承exception,重写析构和what方法

三、C++标准库、模板库

STL六大部件

容器(Containers)

pair<long,string>(1,"2");

分配器(Allocators)

算法(Algorithms)

迭代器(Iterator)

适配器(Adapters)

仿函数(Functors)

四、问题总结

拷贝赋值

检测自我赋值

1. char str[] 和char* str的区别

参考:char str[] 和 char *str 的区别

1
2
3
4
char* get_str(void){
char str[] = {"abcd"};
return str;
}

char str[] = {“abcd”};定义了一个局部字符数组,尽管是数组,但它是一个局部变量,返回它的地址肯定是一个已经释放了的空间的地址。
此函数返回的是内部一个局部字符数组str的地址,且函数调用完毕后 此数组被销毁,所以你返回的指针也就指向一块被销毁的内存,这种写法是错误的。

1
2
3
4
char* get_str(void){
char *str = {"abcd"};
return str;
}

char* str = {“abcd”};表示先定义个字符串常量,并将其地址赋给str。

此函数返回的是字符串常量的地址,而像这种字符串都是属于全局的,在编译的时候就已经分配了内存了,只有程序退出的时候才会被销毁,所以返回它的地址是没有问题的,但是你最好返回常量指针,因为你不能去改变字符串常量的值。
这个str就在栈里面,但后面的字符串在常量区,函数返回的时候,先将常量区的地址返回,然后作用域结束,释放str在栈里面的空间。。

1
2
const char str[] = "abcd";//abc存储在堆栈中
const char *str = "abcd";//abc存储在静态存储区

准确的说,上面两个“abc”都是存储在静态存储区,即常量区。常量区是可读不可写的。所以任何试图对常量区进行写的操作都是非法的,当然了,这也不是一定不可写的,你可以采取某种渠道改变常量区的内存属性,比如改变pe相关节的属性就可以对常量区进行读写,当然了,这个目前可以忽略。。。

那么为什么str[] = “abc”; 可以写呢? 答案就在str[] = “abc”;会有一个额外的拷贝过程,即把常量区的 “abc”拷贝到栈内存去,所以就可以写了。

总结:

  1. 所有以” “或’ ‘包含的字符、字符串都是常量,应该是存储在堆上。
    char *str = "xxxxx",str指向该常量地址。
    char str[] = "xxxxx",str在栈上申请空间。 将常量内容复制进来,所以是局部变量。

  2. 首先,数组和指针是不同的数据类型,有本质的区别:

1
2
char str[] = "abcd";         //sizeof(str) == 5 * sizeof(char)
char * str = "abcd"; //sizeof(str) == 4(x86) or 8(x64)
  1. 数组可以自动转型成指针,指针不能转成数组。
    字符串等价于字符数组,而不等于字符指针。根据上一条,字符串可以自动转型成字符指针。
    再然后,”abcd”叫做“字符串常量”,任何类型的常量都是右值(没有名字的临时变量),必须让”abcd”成为左值(有名字的变量),才能够修改”abcd”这个字符串
1
2
char str[] = "abcd";              //等号两端是相同的数据类型,右值成为左值
char * str = "abcd"; //等号两端是不同的数据类型,右端自动转型成char*,该char*得到了str的名字,而"abcd"这个char数组仍然没有名字。

char * str是存储在全局静态存储区,所以,虽然是局部变量但函数返回后依然可以拿到正确的值!
char str[] 是存储在栈上的,local variable ,函数返回后,OS就收回空间了,就不复存在了,所以,拿不到正确的结果!
char str[]=”name”;与char str[5];str=”name” 的不同之处在哪,能不能从内存分配的角度讲一讲,我知道数组名字是一个常量地址(指针),第一个为什么对,第二个为什么错?

第二个先定义了一个数组,要知道数组名str是数组分配到的空间的首地址,str=”name”应该是等号两边类型不匹配的错误。一般的常量应该没有内存地址的,除非有某个变量指向了该常量。
数组名是地址常量,那么常量当然不允许被重新赋值。
“name”是一个字符串常量他存储在常量存储区,只能用一个指针指向它却不允许改变:char*p;p=”name”;
一般情况下char str[]=”name”;数组是在栈上的空间由编译器分配,内容可以由用户改变。