记录C/C++中常用的函数、易混淆的概念和日常开发记录的笔记,仅作为自己加深印象,强化学习使用。
文章目录
- 1. 分割字符串函数(strtok)
- 2. 错误时结束程序函数(assert)
- 3. 避免同一个头文件包含多次
- 4. C++中的四种强制类型转换
- 5. 在C++程序中调用被C编译器编译后的函数,为什么要加extern“C”?
- 6. 内存溢出 内存泄露 内存越界 堆栈溢出 野指针
- 7. int fun() 和 int fun(void)的区别?
- 8. C++中的继承、封装和多态
- 9. C++的内存有哪几种类型?
- 10. C++中的public、protected和private
- 11. 指针常量与常量指针
- 12. 智能指针:auto_ptr(C++11被弃用)、unique_ptr、shared_ptr和weak_ptr
- 13. C++如何禁止对象的复制、赋值和delete的用法
- 14. override和final
- 15. gcc与g++
- 16. iota
- 17. limits--最大值与最小值
- 18. shared_ptr与unique_ptr对象的构造
1. 分割字符串函数(strtok)
该函数包含在cstring中。
参考地址:http://www.cplusplus.com/reference/cstring/strtok/
这个函数在解析数据时很有用。
- 函数原型:
char *strtok(char s[], const char *delim)
包含在头文件cstring中,s为要分解的字符串,delim为分隔符字符。s指向要分解的字符串,之后再次调用要把s设成NULL。
- 代码示例:
#include <stdio.h>
#include <string.h>
int main ()
{
char str[] ="- This, a sample string.";
char * pch;
printf ("Splitting string \"%s\" into tokens:\n",str);//
pch = strtok (str," ,.-");//第一次拆分,使用str
while (pch != NULL)
{
printf ("%s\n",pch);
pch = strtok (NULL, " ,.-");//第二次拆分,使用NULL
}
return 0;
}
2. 错误时结束程序函数(assert)
assert宏的原型定义在<assert.h>中(或者cassert),该头文件也只有这一个函数
。其作用是如果它的条件返回错误,则终止程序执行。原型如下:
#include <assert.h>
void assert( int expression );
assert的作用是先计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。看下面的例子:
#include <assert.h>
#include<iostream>
using namespace std;
int main( void )
{
assert(0&&"报错了!!!");//也可以assert(0);
cout<<"结尾!!!"<<endl;
return 0;
}
输出:
int main(): Assertion `0&&"报错了!!!"' failed.
说明:
- assert括号内如果为假,那么程序直接结束;
- assert括号中双引号的作用是为了描述错误,对功能没有影响。
3. 避免同一个头文件包含多次
有两种实现方式,一种是#ifndef方式;另一种是#pragma once方式。
- 方式一:
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
... ... // 声明、定义语句
#endif
- 方式二:
#pragma once
... ... // 声明、定义语句
区别:
(1)#ifndef
-
#ifndef
的方式受C/C++语言标准支持。它不仅可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件(或者代码片段)不会被不小心同时包含
。 -
当然,缺点就是如果
不同头文件中的宏名不小心“撞车”
,可能就会导致你看到头文件明明存在,但编译器却硬说找不到声明的状况——这种情况有时非常让人郁闷。 -
由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,
#ifndef
会使得编译时间相对较长
,因此一些编译器逐渐开始支持#pragma once
的方式。
(2)#pragma once
- #pragma once 一般由
编译器提供保证
:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件
。 - 你无法对一个头文件中的一段代码作
#pragma once
声明,而只能针对文件。 - 对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名冲突引发的“找不到声明”的问题,这种重复包含很容易被发现并修正。
#pragma once
方式产生于#ifndef
之后,因此很多人可能甚至没有听说过。目前看来#ifndef
更受到推崇。因为#ifndef
受C/C++语言标准的支持,不受编译器的任何限制;
而#pragma once
方式却不受一些较老版本的编译器支持,一些支持了的编译器又打算去掉它,所以它的兼容性可能不够好。
4. C++中的四种强制类型转换
类型 | 用法 |
---|---|
static_cast | 1. 内置数据类型 的转换;2. 继承关系 的指针或引用;3. 不能用于内置数据类型指针的转换 |
dynamic_cast | 只能用于继承关系且子类转父类 的指针或引用(子类转父类,不会越界) |
const_cast | 增加或者去掉const属性,必须用于指针或引用 |
reinterpret_cast | 任何类型都可以转,包括函数指针、无关数据类型等 |
代码举例:
#include<iostream>
using namespace std;
class Base{
};
class Derive:public Base {
};
void test_01()
{
//基本类型
int a = 10;
double b = static_cast<double>(a);
//继承关系的对象指针,子类和父类可以相互转换,但是需要自己注意类型安全问题
Base *B = nullptr;
Derive *D = nullptr;
D = static_cast<Derive*>(B);//注意Derive*或者Derive&
B = static_cast<Base*>(D);
//不能用于基础类的指针
// int *p = nullptr;
// char *sp = static_cast<char*>(p);
}
void test_02()
{
//只能用于子类转父类
Base *B = nullptr;
Derive *D = nullptr;
B = dynamic_cast<Base*>(D);
//D = dynamic_cast<Derive*>(B);不可以
}
void test_03()
{
//用于去掉const属性
int a = 10;
const int &b = a;
int& c = const_cast<int&>(b);
c = 20;//a,b,c全部修改为20
cout<<"a: "<<a<<endl;
cout<<"b: "<<b<<endl;
cout<<"c: "<<c<<endl;
//指针
const int* p = nullptr;
int* cp = const_cast<int*>(p);
//增加const
int* p1 = nullptr;
const int* p2 = const_cast<const int*>(p1);
}
int main()
{
test_03();
return 1;
}
5. 在C++程序中调用被C编译器编译后的函数,为什么要加extern“C”?
对于下列函数:
void foo(int x, int y);
C++语言支持函数重载,C语言不支持函数重载
,函数被C++编译器编译后在库中的名字与C语言的不同。C编译器编译后在库中的名字为 _foo,而C++编译器则会产生像: _foo_int_int 之类的名字
;- 为了解决此类名字匹配的问题,C++提供了C链接交换指定符号 extern “C”。
6. 内存溢出 内存泄露 内存越界 堆栈溢出 野指针
- 内存泄露
内存泄露是指程序中已动态分配的堆内存,因为某种原因未释放或无法释放内存,造成内存空间的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
- 内存溢出
应用系统中存在
无法回收的内存或使用的内存过多
,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件
。
- 内存越界
向系统申请了一块内存,而在
使用的时候超过了申请的范围
,常见的如数组访问越界
- 堆栈溢出
- 堆栈溢出一般包括堆内存溢出和栈内存溢出,两者都属于缓冲区溢出。
- 堆内存溢出:可能是堆的尺寸设置的太小、动态申请的内存没有释放等。
- 栈内存溢出:可能是栈的尺寸设置的太小、递归层次太深、函数调用层次过深、分配了过大的局部变量等
- 野指针
- 野指针就是指针
指向的位置是不可知
的(随机的、不正确的、没有明确限制的)。指针变量在定义时如果未初始化,其值是随机的。
造成野指针的原因
- 指针变量没有初始化,任何刚创建的指针不会自动生成为NULL,需要手动设置。
- 指针被free或者delete之后没有设置NULL
- 指针操作超越变量作用域。所以函数中不要返回指向栈内存的指针或者引用,因为栈内存在函数结束的时候会被释放。
7. int fun() 和 int fun(void)的区别?
- 在C++中没有区别,都是输入void类型,返回int类型的函数。
- 在C中则有区别,这两个函数都返回int类型(即使函数前面没有int,也返回int),但是括号中没有内容表示
输入类型和个数没有限制
,第二个则表示函数输入类型为void。
8. C++中的继承、封装和多态
- 继承的目的是为了提高代码的复用性和可扩展性;
- 封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员;
- 多态的目的是实现了动态联编,使程序运行效率更高,更容易维护和操作。
这也是C++和C语言区别,C语言是面向过程的语言,它的核心是函数,而C++是面向对象的语言,他的核心是类和对象。其实,C++是C语言的超集。
9. C++的内存有哪几种类型?
堆(malloc)、栈(stack)、程序代码区、全局/静态存储区、常量存储区。
-
堆区:由
new申请分配的内存块
,我们通过应用程序来动态控制它们的申请和释放。如果程序没有正确释放它们,那么程序结束后,由操作系统自动回收。 -
栈区:由
编译器自动申请和释放的内存块
,通常用来存储局部变量、临时变量、函数参数
。执行效率高,但是分配的内存容量有限。 -
程序代码区:存放程序二进制代码。
-
全局/静态存储区: 全局变量和静态变量是存储在一块的,
程序结束后由系统释放
。 -
常量存储区:存储常量的内存块,不允许被修改。
10. C++中的public、protected和private
- 类的一个特征就是封装,public和private作用就是实现这一目的。用户代码(类外)可以访问public成员,而不能访问private成员;private成员只能由类成员(类内)和友元访问。
- 类的另一个特征就是继承,protected的作用就是实现这一目的。所以:protected成员可以被派生类访问(
只能在派生类的内部,也就是成员函数的形式访问,不能定义派生类对象来访问
),不能被用户代码(类外)访问。
有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。
public继承:基类public成员,protected成员,private成员的访问属性在派生类中分别变成:public, protected, private
protected继承:基类public成员,protected成员,private成员的访问属性在派生类中分别变成:protected,protected, private
private继承:基类public成员,protected成员,private成员的访问属性在派生类中分别变成:private, private, private
11. 指针常量与常量指针
- 常量指针:
指向常量
的指针,也就是说指向的是常量而不是变量,指针本身可以改变(即可以指向另外一个常量),但是指向的内容不能使用指针对其进行修改
。- 指针常量:
指向指针
,本身是常量,也就是说它的指向不能被改变
,但是指向的内容可以改变。
使用的时候,记住3句话就可以了:
- *象征着地址,const象征着内容;
- 指针和 const 谁在前先读谁 ;
- 谁在前面,谁就不允许改变。
常量指针举例:
#include <iostream>
using namespace std;
int main()
{
int a = 2;
int b = 4;
int const *p = &a; //定义常量指针
a = 5; //正常,此时*p的内容为5,但指向不变
//*p = 3;//报错,不能改变常量指针的内容
p = &b; //正常,可以修改指向
}
指针常量举例:
#include<iostream>
using namespace std;
int main()
{
int a = 2;
int b = 4;
int * const p = &a;//定义指针常量
*p = 3;//正常,可以修改指针常量的内容
p = &b;//报错,不可以修改指针常量的指向
}
12. 智能指针:auto_ptr(C++11被弃用)、unique_ptr、shared_ptr和weak_ptr
- 为什么使用智能指针
先来看一段代码:
void someFunction()
{
Resource* ptr = new Resource; // Resource是一个类或者结构
// 使用ptr处理
// ...
delete ptr;
}
如果在delete ptr之前,函数被return或者抛出异常等情况,普通指针的删除将不会被执行,从而导致内存泄漏
。那么,智能指针就被排上用场了。
- 智能指针的实现
智能指针的思想:将动态申请的资源交给一个类变量来保存,由于类变量在局部作用域,其离开后将会自动调用析构函数
。
- auto_ptr
auto_ptr虽然可以自动删除指针,但是有一个问题,当两个以上指针同时指向同一个地址的时候,当指针过期的时候,程序中的将试图删除同一个对象多次,这将是不被允许的,例如下列会出现删除多次的情况:
auto_ptr<string> p1 (new string ("I reigned lonely as a cloud."));
auto_ptr<string> p2;
p2 = p1; //auto_ptr不会报错.
说明以下例子:p2接管string对象的所有权后,p1的所有权将被剥夺
。可防止p1和p2的析构函数试图刪同—个对象,但是,有一个问题,如果后面继续使用p1将会出现问题,因为p1不再指向有效的数据。
有以下方案解决:
定义陚值运算符
,使之执行深复制
。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。建立所有权(ownership)概念
。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr的策略,但unique_ptr的策略更严格。创建智能更高的指针
,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。
- unique_ptr
unique_ptr和auto_ptr类似,也是剥夺所有权,但是unique_ptr是唯一的,编译阶段就能检测出错误
。
unique_ptr<string> p3 (new string ("auto"); //#4
unique_ptr<string> p4; //#5
p4 = p3; //#6,编译器检测出错误,不能指向同一个地址。
它比auto_ptr聪明的另外一个地方,就是允许临时右值
(包括临时指针,std::move操作)。程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做。例如下列是允许的。
unique_ptr<string> demo(const char * s)
{
unique_ptr<string> temp (new string (s));
return temp;//临时变量,允许
}
unique_ptr<string> ps;
ps = demo('Uniquely special");//调用
- shared_ptr
前面也介绍了,shared_ptr的策略是:赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete
。
它有下列成员函数:
use_count
: 返回引用计数的个数unique
: 返回是否是独占所有权( use_count 为 1)swap
: 交换两个 shared_ptr 对象(即交换所拥有的对象)reset
: 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少get
: 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.
使用方法举例:
#include<iostream>
#include<memory>
using namespace std;
int main()
{
string *s1 = new string("s1");
shared_ptr<string> ps1(s1);
cout << ps1.unique()<<endl; //此时是唯一的,true
shared_ptr<string> ps2;
ps2 = ps1;
cout << ps1.unique()<<endl; //此时不唯一,false
cout << ps1.use_count()<<endl; //2
cout<<ps2.use_count()<<endl; //2
string *s3 = new string("s3");
shared_ptr<string> ps3(s3);
cout << (ps1.get()) << endl; //033AEB48
cout << ps3.get() << endl; //033B2C50
swap(ps1, ps3); //交换所拥有的对象
cout << (ps1.get())<<endl; //033B2C50
cout << ps3.get() << endl; //033AEB48
cout << ps1.use_count()<<endl; //1,因为和ps3交换了
cout << ps2.use_count() << endl; //2
ps2 = ps1;//ps2和ps1又指向同一空间
cout << ps1.use_count()<<endl; //2
cout << ps2.use_count() << endl; //2
ps1.reset(); //放弃ps1的拥有权,引用计数减少
cout << ps1.use_count()<<endl; //0
cout << ps2.use_count()<<endl; //1
}
- weak_ptr
shared_ptr虽然已经很好用了,但是有一点shared_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏
。
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
class B; //声明
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout << "A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout << "B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
cout << pb.use_count() << endl; //1
cout << pa.use_count() << endl; //1
pb->pa_ = pa;
pa->pb_ = pb;
cout << pb.use_count() << endl; //2
cout << pa.use_count() << endl; //2
}
int main()
{
fun();
return 0;
}
可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减1,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A、B的析构函数没有被调用)运行结果没有输出析构函数的内容,造成内存泄露。如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_,改为weak_ptr pb_ ,运行结果如下:
1
1
1
2
B delete
A delete
13. C++如何禁止对象的复制、赋值和delete的用法
禁止复制操作可以使得每个对象独一无二。
方法1
:将拷贝构造函数与赋值构造函数声明为private
。但是对于友元函数和类成员函数来说,还是可以调用其相关的复制操作的。方法2
:使用delete关键字显式指示编译器不生成函数的默认版本。(被=delete的成员函数不能被调用
)
- 方法1:将复制相关的操作设置为private私有
class CPeople
{
// ...
private:
// 将复制相关的操作定义为私有
CPeople(){
...};
const CPeople& operator=(const CPeople& rhis){
...}
};
- 方法2:使用delete关键字,显式指示编译器不生成函数的默认版本。
class MyClass
{
public:
MyClass()=default;//默认构造函数
MyClass(const MyClass&) = delete; //阻止拷贝
MyClass & operator = (const MyClass&) = delete; //阻止赋值
......
}
delete的另一种用法:
下列如果没有void func(double data)=delete;则会使用隐式类型转换调用 void func(int data) 。
#include <cstdio>
class TestClass
{
public:
void func(int data) {
printf("data: %d\n", data); }
void func(double data)=delete;//禁止使用double类型
};
int main(void)
{
TestClass obj;
obj.func(100);
obj.func(100.0);//错误,浮点类型被禁止调用
return 0;
}
14. override和final
override
:它指定了子类的这个虚函数是重写的父类函数
。final
:当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错
。
override举例:
class A
{
virtual void foo();
}
class B :public A
{
void foo(); //OK,方式1
virtual foo(); // OK,方式2
void foo() override; //OK,方式3
}
final举例:
class Base
{
virtual void foo();
};
class A : Base
{
void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
void bar() final; // Error: 父类中没有 bar虚函数可以被重写或final
};
class B final : A // 指明B是不可以被继承的
{
void foo() override; // Error: 在A中已经被final了
};
class C : B // Error: B is final
{
};
15. gcc与g++
- gcc:C语言使用
- g++:C和C++均可使用
在使用std::cout时,它是c++中的语法,gcc会编译出错,所以要用g++。
16. iota
该函数在头文件numeric
中,功能为向序列中写入
以val为初值的连续值序列
#include <numeric> // std::iota
int numbers[10];
std::iota(numbers, numbers + 10, 100);//以100为初值
//numbers的值为:100 101 102 103 104 105 106 107 108 109
建立索引的时候比较好用:
std::vector<int> pc_indices(cloud_ptr->size());
std::iota(pc_indices.begin(), pc_indices.end(), 0);
17. limits–最大值与最小值
#include <iostream> // std::cout
#include <limits> // std::numeric_limits
int main () {
std::cout << std::boolalpha;
std::cout << "int类型的最小值: " << std::numeric_limits<int>::min() << '\n';
std::cout << "int类型的最大值: " << std::numeric_limits<int>::max() << '\n';
std::cout << "int是有符号: " << std::numeric_limits<int>::is_signed << '\n';
std::cout << "int的位数: " << std::numeric_limits<int>::digits << '\n';
std::cout << "int 是无限大小: " << std::numeric_limits<int>::has_infinity << '\n';
return 0;
}
输出:
int类型的最小值: -2147483648
int类型的最大值: 2147483647
int是有符号: true
int的位数: 31
int 是无限大小: false
18. shared_ptr与unique_ptr对象的构造
shared_ptr和unique_ptr的头文件在#include <memory>
。
- shared_ptr对象的创建
//方式1:ptr相当于一个nullptr,ptr.use_count == 0
std::shared_ptr<T> ptr;
std::shared_ptr<int> ptr (nullptr);
//方式2:new方法,ptr.use_count == 1
std::shared_ptr<T> ptr(new T);
//方式3:使用复制构造函数,ptr2.use_count++
std::shared_ptr<T> ptr2(ptr1);
//方式4:赋值
std::shared_ptr<T> a(new T());
std::shared_ptr<T> b(new T());
a = b; // 此后 a 原先所指的对象会被销毁,b 所指的对象引用计数加 1
//方式5:重置方法reset。这里也可以先ptr=nullptr
std::shared_ptr<T> ptr(new T());
ptr.reset(new T()); 重置指针,并接管新的堆空间对象
//方式6:make_shared
std::shared_ptr<int> ptr = std::make_shared<int> (10);//等价于std::shared_ptr<int> foo2 (new int(10));
auto ptr = std::make_shared<int> (20);
- 父类与子类的智能指针
Base::Ptr infer_ptr_;//自定义的父类
infer_ptr_ = std::shared_ptr<Base>(new Derived);//指向子类对象
有文章指出,不能使用原始指针初始化多个shared_ptr,这个说法是错误的
,举例如下:
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<int> p(new int(10));
std::shared_ptr<int> p1(p);
std::shared_ptr<int> p2(p);
std::cout << "use_count:\n";
std::cout << "p1: " << p.use_count() << '\n';
std::cout << "p2: " << p1.use_count() << '\n';
std::cout << "p3: " << p2.use_count() << '\n';
return 0;
}
输出:
use_count:
p1: 3
p2: 3
p3: 3
- unique_ptr的构造
//方法1:定义空指针
std::unique_ptr<int> ptr;
std::unique_ptr<int> ptr (nullptr);
//方法3:new方法
std::unique_ptr<int> ptr (new int);
//方法4:赋值
std::unique_ptr<int> ptr;
ptr = std::unique_ptr<int>(new int (101)); // rvalue
//方法5:reset方法
std::unique_ptr<int> ptr; // empty
ptr.reset (new int); // takes ownership of pointer
//方法6:move
std::unique_ptr<int> foo = std::unique_ptr<int>(new int (101));
std::unique_ptr<int> bar = bar = std::move(foo); // using std::move
文章评论