c++相关知识速看
相关网站:
https://isocpp.org/:C++标准的权威官网。ISO(International Organization for Standardization)表示国际标准化组织,该组织不仅标准化了C++标准,还标准化其他一些行业标准,比如质量管理体系ISO 9001、java语言标准ISO/IEC 8652:1995等。
https://cplusplus.com/reference/:非权威官网,但是值得一看,内有c++标准库(Standard C++ Library reference)接口文档。只是将标准库,和特定实现(比如GNU C++ Library或者Microsoft Visual C++ Standard Library)没关系。
https://en.cppreference.com/:非权威官网,但是值得一看
https://www.learncpp.com/:学习C++
https://gcc.gnu.org/:gcc官网
1 数据类型
1.1 基本数据类型
介绍略
初始化方式有3种
- int x=0; //类c方式
- int x (0); //构造器方式
- int x {0}; //初始化列表,c++11之后才支持
类型演绎
int x=0; auto y=x;//等效于int y=x; int x=0; decltype(y) x;//等效于int y;
1.2 复合数据类型
比如string类型、struct类型、class类型
2 函数
2.1 值传递和引用传递
函数的值传递和引用传递,引用是变量的别名。
void duplicate(int &a, int &b, int &c) { a *= 2; b *= 2; c *= 2; } int main() { int x = 1, y = 3, z = 7; duplicate(x, y, z); cout << "x=" << x << ", y=" << y << ", z=" << z; return 0; }
有时候,当函数参数是大型的复合类型时,采用值传递的方式往往会带来打的开销,此时,我们可以采用引用传递的方式,然而,引用传值可能会修改引用所指向的对象,为避免此情况,我们往往将引用类型定义为常量。
string concatenate (const string& a, const string& b) { return a+b; }
2.2 内联函数
使用普通的函数,程序运行时会多出一些堆栈操作以及跳转,对于短而小的函数,我们可以将其声明为内联函数,告诉编译器将函数代码放到调用处,这样程序执行时会少一些函数调用的开销。
inline string concatenate (const string& a, const string& b) { return a+b; }
2.3 函数模板
#include <iostream> using namespace std; template <class T> T sum(T a, T b) { T result; result = a + b; return result; } int main() { int i = 5, j = 6, k; double f = 2.0, g = 0.5, h; k = sum<int>(i, j); h = sum<double>(f, g); cout << k << '\n'; cout << h << '\n'; return 0; }
3 名称空间
这里我们使用了3个名称空间,std,foo,bar
#include <iostream> using namespace std; namespace foo { int value() { return 5; } } namespace bar { const double pi = 3.1416; double value() { return 2*pi; } } int main () { cout << foo::value() << '\n'; cout << bar::value() << '\n'; cout << bar::pi << '\n'; return 0; }
使用using关键字指定名称空间(可指定整个名称空间、指定名称空间中的单个成员、指定名称空间在特定代码块中有效,见下面3个例子)
第一个例子,指定整个名称空间
#include <iostream> using namespace std; namespace first { int x = 5; int y = 10; } namespace second { double x = 3.1416; double y = 2.7183; } int main () { using namespace first; cout << x << '\n'; cout << y << '\n'; cout << second::x << '\n'; cout << second::y << '\n'; return 0; }
第二个例子,指定名称空间的特定成员
#include <iostream> using namespace std; namespace first { int x = 5; int y = 10; } namespace second { double x = 3.1416; double y = 2.7183; } int main () { using first::x; using second::y; cout << x << '\n'; cout << y << '\n'; cout << first::y << '\n'; cout << second::x << '\n'; return 0; }
第三个例子,名称空间在单个代码块中有效
#include <iostream> using namespace std; namespace first { int x = 5; } namespace second { double x = 3.1416; } int main () { { using namespace first; cout << x << '\n'; } { using namespace second; cout << x << '\n'; } return 0; }
std名称空间
C++标准库中的成员都在std名称空间下,比如之前我们看到的
std::cout << "Hello world!";
4 数组:C数组、<array>、<vector>
C++中有三种类型的数组
- 原生数组
- std::vector 动态数组,且自动管理内存
- std::array 静态数组,c++11新特性
原生数组作为参数传递时,它其实传递的是指向第一个元素的指针。
void printArray(int *arr, size_t size)
{
for (size_t i = 0; i < size; ++i)
{
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
int main()
{
int myArray[5] = {1, 2, 3, 4, 5};
// 调用函数,传递数组名和数组大小
printArray(myArray, sizeof(myArray) / sizeof(myArray[0]));
return 0;
}
5 字符串
字符串,也就是字符序列
当我们定义一个字符串字面值时,其实我们定义了一个字符数组,后跟一个空字符(空字符ascii码为0,使用''表示,也可使用转义字符\0表示)
如下两种方式分别定义了一个字符序列后跟一个空字符和一个字符串字面量。他们两个在内存存储方面是等效的。
char myword1[] = { 'H', 'e', 'l', 'l', 'o', '\0' }; char myword2[] = "Hello";
但是需要注意的是,我们不能修改其字符数组。比如,如下方式会有编译错误
myword = "Bye"; myword[] = "Bye"; myword = { 'B', 'y', 'e', '\0' };
然而std::string定义的字符串,则可以修改字符串的内容。因为std::string是一个类,这个类中存储了它所代表的字符串以及修改它的方法,因此,我们可以使用=来修改其内容。
std::string myword3 = "Hello"; myword3 = "Bye";
6 指针
假设,我们把25赋值给myvar变量。然后将myvar变量的地址赋值给foo。存储其他变量地址的变量我们称之为指针。
myvar = 25; foo = &myvar; bar = myvar;
上述示例中,我们可以看到foo和bar的区别是foo使用取址运算符&进行取址,而bar是直接赋值操作
然后我们介绍解引用运算符*,它用在指针变量前面,用于表示该指针指向的变量的值。
我们看下面例子,baz1是普通的赋值操作,baz1变量的值为1776,而baz2是*foo的值,也就是foo指针指向的变量myvar变量的值,也就是25.
baz1 = foo; // baz1 equal to foo (1776) baz2 = *foo; // baz2 equal to value pointed to by foo (25)
6.1 声明指针
通过指针的解引用操作我们可以得到指针指向变量的值,因此我们声明指针时就需要声明指针指向变量的类型。指针变量的声明举例:
int * number;
我们声明的是一个指向int类型数据的指针。需要注意的是我们声明的这个类型并不是指针变量本身的类型,而是指针变量所指向的变量的类型。
需要注意的是,解引用运算符和声明指针时的符号都是*,但是其实他们具有不同的意义,这里不要混淆。
int main() { int firstvalue = 5, secondvalue = 15; int *p1, *p2; p1 = &firstvalue; // p1 = address of firstvalue p2 = &secondvalue; // p2 = address of secondvalue *p1 = 10; // value pointed to by p1 = 10 *p2 = *p1; // value pointed to by p2 = value pointed to by p1 p1 = p2; // p1 = p2 (value of pointer is copied) *p1 = 20; // value pointed to by p1 = 20 cout << "firstvalue is " << firstvalue << '\n'; cout << "secondvalue is " << secondvalue << '\n'; return 0; }
6.2 指针和数组
数组的工作方式非常类似于指向其第一个元素的指针,而且,实际上,数组总是可以隐式地转换为适当类型的指针
int main() { int numbers[5]; int *p; p = numbers; *p = 10; p++; *p = 20; p = &numbers[2]; *p = 30; p = numbers + 3; *p = 40; p = numbers; *(p + 4) = 50; for (int n = 0; n < 5; n++) cout << numbers[n] << ", "; return 0; }
//输出为:10, 20, 30, 40, 50,
6.3 指针和const
有时候我们希望通过指针变量可以访问其指向的变量的值,但是不希望通过指针修改它。那么我们可以通过const来声明指针变量。
int x; int y = 10; const int *p = &y; x = *p; // ok: reading p *p = x; // error: modifying p, which is const-qualified
此性质常常用于函数传值
void increment_all(int *start, int *stop) { int *current = start; while (current != stop) { ++(*current); // increment value pointed ++current; // increment pointer } } void print_all(const int *start, const int *stop) { const int *current = start; while (current != stop) { cout << *current << '\n'; ++current; // increment pointer } } int main() { int numbers[] = {10, 20, 30}; increment_all(numbers, numbers + 3); print_all(numbers, numbers + 3); return 0; }
6.4 指针和字符串常量
前面讲过,字符串常量其实就是一个字符序列+空字符(\0);前面还讲过指针和数组的关系,数组往往可以隐式的转化为指针(比如: int numbers[] = {10, 20, 30};cout<<*(numbers+1);//输出为20 )。因此我们可以这样定义一个指针,其指向字符串常量
const char * foo = "hello";
其内存示意图如下
需要注意的是foo是一个指针,其值是1702,其并不是字符'h',也不是字符串常量"hello"。下面两种访问字符的方式是一样的,都是字符'o'
*(foo+4) foo[4]
6.5 指向指针的指针
void pointer
null pointer
指向函数的指针
7 动态内存
有时,我们需要在程序运行期间动态的申请和释放内存,此时我们使用new关键字和delete关键字进行申请和释放内存
int main() { int * foo; foo1 = new int;//申请一个int大小的内存 foo2 = new int [5];//申请5个int大小的内存 *foo2=1; foo2[1]=2; *(foo2+2)=3; cout<<foo2[0]<<foo2[1]<<foo2[2]; delete foo1; //释放内存 delete[] foo2; return 0; }
有时,new申请内存会失败,使用如下两种方式来检查:
- 捕获bad_alloc异常
- 使用foo = new (nothrow) int [5];方式申请内存,然后使用 if (foo == nullptr) 判断是否分配失败
疑问,动态内存需要手动处理,那比如类的对象是否需要手动处理呢?如果不是用new创建的对象则不需要,此时内存分配是在栈中进行的,操作系统会自动进行清理,而使用new创建的对象是在堆上进行的。在一个进程中堆向上生长,栈向下生长。
在C++中,使用关键字new申请内存,而在C中则使用库函数(cstdlib库)的方式:malloc、calloc、free等。
8 结构体
struct product { int weight; double price; } ; product apple; product banana, melon;
8.1 指向结构体的指针
如果一个指针指向结构体,则我们可以使用p->方式访问成员,其等效于(*p)方式
struct movies_t { string title; int year; }; int main() { movies_t amovie; movies_t *pmovie; pmovie = &amovie; pmovie->title = "阿甘正传"; pmovie->year = 2004; //如下两种方式与pmovie->title等效 amovie.title = "泰坦尼克号"; (*pmovie).title = "乱世佳人"; cout << pmovie->title << " " << pmovie->year; return 0; }
9 类
定义一个类和定义一个结构体基本一致,区别:
- 类使用class或struct定义
- 类中可包含函数
- 类成员有3种访问权限:private(默认),protected,public
class Rectangle { int width, height; public: void set_values(int, int); int area() { return width * height; } }; void Rectangle::set_values(int x, int y) { width = x; height = y; } int main() { Rectangle rect; rect.set_values(3, 4); cout << "area: " << rect.area(); return 0; }
此处,我们使用作用域操作符(::)在类的外面定义类成员函数。通过::操作符告诉编译器,这个函数是类成员函数,而不是普通函数。
9.1 构造器
上面例子,如果我们不调用rect.set_values(3, 4);而直接调用area()函数,输出结果是不可预知的。因此需要对成员变量做一些必要的初始化,此时就用到构造器了
构造器不需要注明返回类型,且其要和类名相同。
class Rectangle { int width, height; public: Rectangle(int, int); int area() { return (width * height); } }; Rectangle::Rectangle(int a, int b) { width = a; height = b; } int main() { Rectangle rect(3, 4); cout << "rect area: " << rect.area() << endl; return 0; }
默认构造器
class Rectangle { int width, height; public: Rectangle (); Rectangle (int,int); int area (void) {return (width*height);} }; Rectangle::Rectangle () { //默认构造器 width = 5; height = 5; } Rectangle::Rectangle (int a, int b) { width = a; height = b; } int main () { Rectangle rect (3,4); //调用指定构造器 Rectangle rectb; //调用默认构造器 cout << "rect area: " << rect.area() << endl; cout << "rectb area: " << rectb.area() << endl; return 0; }
9.2 一致性初始化
c++11引入的新特性,也叫uniform init
class Circle { double radius; public: Circle(double r) { radius = r; } double circum() {return 2*radius*3.14159265;} }; int main () { Circle foo (10.0); // functional form 直接初始化 Circle bar = 20.0; // assignment init.赋值初始化,对于只有单个成员变量的类,可以使用类似赋值的方式初始化 Circle baz {30.0}; // uniform init. 统一初始化 Circle qux = {40.0}; // POD-like 类POD初始化 cout << "foo's circumference: " << foo.circum() << '\n'; return 0; }
9.3 成员初始化列表
在C++中,成员初始化列表(Member Initialization List)是一种在构造函数体之前初始化类成员的方法。
成员初始化列表的语法允许你以清晰的方式列出所有需要初始化的成员,并且它是构造函数体执行之前的第一个操作。这有助于确保你的对象在构造函数体中的任何代码执行之前就已经处于一个有效且一致的状态。
class Circle { private: double radius; // 半径 const int precision; // 精度,一个常量成员 std::string description; // 描述 public: // 构造函数,使用成员初始化列表 Circle(double r, int prec, const std::string& desc) : radius(r), precision(prec), description(desc) { // 构造函数体可以为空,因为成员已经在成员初始化列表中初始化了 } // 其他成员函数... void printInfo() const { std::cout << "Circle: Radius = " << radius << ", Precision = " << precision << ", Description = " << description << std::endl; } }; int main() { Circle myCircle(5.0, 10, "A perfect circle"); myCircle.printInfo(); return 0; }
9.4 指向类实例的指针
指向对象的指针,可以像结构体一样使用->访问成员变量和函数
class Rectangle { int width, height; public: Rectangle(int x, int y) : width(x), height(y) {} int area(void) { return width * height; } }; int main() { Rectangle obj (3, 4);//普通初始化 Rectangle * foo, * bar, * baz;//定义三个指针 foo = &obj; bar = new Rectangle (5, 6);//使用new动态分配内存 baz = new Rectangle[2] { {2,5}, {3,6} };//使用new动态分配内存,并使用一致性初始化方式初始化实例 cout << "obj's area: " << obj.area() << '\n'; cout << "*foo's area: " << foo->area() << '\n'; cout << "*bar's area: " << bar->area() << '\n'; cout << "baz[0]'s area:" << baz[0].area() << '\n'; cout << "baz[1]'s area:" << baz[1].area() << '\n'; delete bar;//使用delete回收内存 delete[] baz;//使用delete回收内存 return 0; }
9.5 this关键字
用在类的成员函数中表示指向对象本身的指针
class Circle { double radius; public: Circle(double r) { this->radius = r; } double circum() { return 2 * this->radius * 3.14159265; } }; int main () { Circle foo {10.0}; cout << "foo's circumference: " << foo.circum() << '\n'; return 0; }
9.6 类的静态成员
类的静态成员(包括静态数据成员和静态成员函数)是属于类本身的,而不是类的某个特定对象。这意味着静态成员在类的所有对象之间共享,无论创建了多少个对象,静态成员都只有一份拷贝
静态数据成员必须在类定义体的外部进行初始化
1 静态数据成员
class MyClass { public: static int count; // 静态数据成员 MyClass() { count++; // 每当创建新对象时,count递增 } }; int MyClass::count = 0; // 静态数据成员初始化 int main() { MyClass obj1, obj2, obj3; std::cout << "Object count: " << MyClass::count << std::endl; // 输出3 return 0; }
2 静态成员函数
class MyClass { public: static int staticValue; static void printValue() { std::cout << "Static value: " << staticValue << std::endl; } }; int MyClass::staticValue = 42; int main() { MyClass::printValue(); // 调用静态成员函数 return 0; }
10 预处理指令
10.1 #include
预处理指令#include,在程序中预处理指令用于在编译之前进行一些处理。比如,#include <iostream>,告诉预处理器嵌入一段标准c++代码,也就是会将/usr/include/c++/7/iostream文件中的内容嵌入到当前位置。也就是说我们去掉#include <iostream>,而使用/usr/include/c++/7/iostream代码替代也是完全可以的
10.2 宏定义
宏定义使用#define和#undef进行定义,由预处理器进行处理。定义的宏不受块结构的影响。宏的有效性一直持续到#undef为止
#define TABLE_SIZE 100 //宏定义 int table1[TABLE_SIZE]; #undef TABLE_SIZE //使用#undef使宏定义失效 #define getmax(a,b) a>b?a:b //函数宏 int main() { *(table1+1)=1; cout << table1[1] << endl; int x=5, y; y= getmax(x,2); cout << y << endl; #undef getmax //使用#undef使宏定义失效 int z = getmax(x,2); //此处在进行预处理时,预处理器将报错 return 0; }
10.3 条件编译
#define DEBUG #ifdef DEBUG #include <iostream> void debugPrint(const std::string& msg) { std::cout << "Debug: " << msg << std::endl; } #else void debugPrint(const std::string& msg) {} #endif int main() { debugPrint("aaaaaaa"); }
11 c++中的标准库
标准库由C++标准委员会制定和维护(只是标准,不包含实现)
组成:
- c库
- std库(标准模板库)
- <iostream>库
- <ctime>库
- 其他
c++标准库在linux中是由gnu的libstdc++库实现的,在windows中是由Microsoft Visual C++(MSVC)实现的。
我们在linux中做c++开发时,调试过程中的标准库源码就是在系统的某个路径下面,比如:/usr/include/c++/7.5.0/