C/C++ Edge Cases and Testing Patterns
C and C++-specific edge cases, common pitfalls, and testing patterns focusing on memory safety, undefined behavior, and low-level concerns.
Table of Contents
- Pointer and Memory Edge Cases
- Integer Overflow and Undefined Behavior
- Array and Buffer Edge Cases
- String Edge Cases
- Type Conversion Edge Cases
- Undefined Behavior
- C++ Specific Edge Cases
- Testing Frameworks
Pointer and Memory Edge Cases
Null Pointer Dereference
#include <assert.h>
#include <stdlib.h>
#include <string.h>
void test_null_pointer_edge_cases() {
// Null pointer dereference (undefined behavior)
int *ptr = NULL;
// Dereferencing null is undefined behavior
// *ptr = 42; // Segmentation fault or worse!
// Safe null check
if (ptr != NULL) {
*ptr = 42;
}
// Null in function calls
// strlen(NULL); // Undefined behavior (likely segfault)
// Safe string handling
const char *str = NULL;
int len = (str != NULL) ? strlen(str) : 0;
assert(len == 0);
}
Dangling Pointers
void test_dangling_pointer_edge_cases() {
int *ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);
// ptr is now dangling!
// Using dangling pointer is undefined behavior
// int value = *ptr; // Use after free!
// Best practice: set to NULL after free
ptr = NULL;
// Now safe to check
if (ptr != NULL) {
int value = *ptr;
}
}
Memory Leaks
void test_memory_leak_edge_cases() {
// Leak: allocated but never freed
int *leak = malloc(100 * sizeof(int));
// ... no free()
// Lost reference leak
int *ptr = malloc(sizeof(int));
ptr = malloc(sizeof(int)); // First allocation leaked!
free(ptr);
// Conditional leak
int *conditional = malloc(sizeof(int));
if (some_condition()) {
free(conditional);
conditional = NULL;
}
// Leaked if condition is false!
// Always free on all paths
if (conditional != NULL) {
free(conditional);
}
}
Buffer Overflow
void test_buffer_overflow_edge_cases() {
char buffer[10];
// Stack buffer overflow
// strcpy(buffer, "This is longer than 10 chars"); // Overflow!
// Safe copy with bounds check
strncpy(buffer, "This is longer than 10 chars", sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0'; // Ensure null termination
// Off-by-one error
for (int i = 0; i <= 10; i++) { // Wrong! i should be < 10
// buffer[i] = 'x'; // Buffer overflow when i == 10
}
// Correct
for (int i = 0; i < 10; i++) {
buffer[i] = 'x';
}
}
Integer Overflow and Undefined Behavior
Signed Integer Overflow
#include <limits.h>
#include <stdio.h>
void test_integer_overflow_edge_cases() {
// Signed integer overflow is undefined behavior in C!
int max = INT_MAX;
// Undefined behavior
// int overflow = max + 1; // UB!
// Check before operation
int a = INT_MAX;
int b = 1;
if (a > 0 && b > 0 && a > INT_MAX - b) {
// Overflow would occur
assert(1); // Detected
} else {
int sum = a + b;
}
// Unsigned overflow wraps around (defined behavior)
unsigned int umax = UINT_MAX;
unsigned int uwrap = umax + 1;
assert(uwrap == 0); // Wraps to 0
// Multiplication overflow check
int x = 100000;
int y = 100000;
if (x != 0 && y != 0 && (INT_MAX / x) < y) {
// Overflow would occur
assert(1); // Detected
}
}
Integer Division Edge Cases
void test_division_edge_cases() {
// Division by zero is undefined behavior
int x = 10;
int y = 0;
// Check before dividing
if (y != 0) {
int result = x / y;
} else {
// Handle divide by zero
}
// Integer division truncates toward zero
assert(7 / 2 == 3);
assert(-7 / 2 == -3);
// Modulo with negative numbers
assert(7 % 3 == 1);
assert(-7 % 3 == -1);
assert(7 % -3 == 1);
// Special case: INT_MIN / -1 overflows!
int min = INT_MIN;
// int overflow = min / -1; // Undefined behavior!
}
Array and Buffer Edge Cases
Array Bounds
void test_array_bounds_edge_cases() {
int arr[5] = {1, 2, 3, 4, 5};
// Valid access
assert(arr[0] == 1);
assert(arr[4] == 5);
// Out of bounds (undefined behavior)
// int oob = arr[5]; // Past end
// int neg = arr[-1]; // Before start
// Pointer arithmetic
int *ptr = arr;
assert(*ptr == 1);
assert(*(ptr + 4) == 5);
// Going past end
// int bad = *(ptr + 5); // Undefined behavior
}
Variable Length Arrays (C99)
void test_vla_edge_cases(int n) {
// VLA with zero or negative size
if (n <= 0) {
// Don't create VLA!
return;
}
// Variable length array
int vla[n];
// Very large VLA can overflow stack
// int huge_vla[1000000]; // Stack overflow!
// Use heap for large allocations
if (n > 1000) {
int *heap_array = malloc(n * sizeof(int));
if (heap_array == NULL) {
// Allocation failed
return;
}
// Use array...
free(heap_array);
} else {
// Small arrays on stack are OK
int stack_array[n];
}
}
String Edge Cases
C String Operations
#include <string.h>
void test_string_edge_cases() {
// Empty string
char empty[] = "";
assert(strlen(empty) == 0);
assert(empty[0] == '\0');
// Null vs empty
char *null_str = NULL;
// strlen(null_str); // Undefined behavior!
// Safe string length
int safe_len = (null_str != NULL) ? strlen(null_str) : 0;
assert(safe_len == 0);
// String without null terminator
char no_null[5] = {'H', 'e', 'l', 'l', 'o'};
// strlen(no_null); // Undefined! Reads past end
// Proper null-terminated string
char proper[6] = "Hello";
assert(strlen(proper) == 5);
// strcpy buffer overflow
char dest[5];
// strcpy(dest, "Too long"); // Buffer overflow!
// Safe copy
strncpy(dest, "Too long", sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';
}
String Comparison
void test_string_comparison_edge_cases() {
char *s1 = "hello";
char *s2 = "hello";
char s3[] = "hello";
// Pointer comparison (wrong!)
// May be true or false depending on string interning
if (s1 == s2) {
// May or may not execute
}
// Correct string comparison
assert(strcmp(s1, s3) == 0);
// Null in comparison
char *null_str = NULL;
// strcmp(null_str, "test"); // Undefined behavior!
// Safe comparison
int cmp = (null_str != NULL && strcmp(null_str, "test") == 0);
assert(cmp == 0);
// Case-insensitive comparison
assert(strcasecmp("Hello", "HELLO") == 0); // POSIX only
}
Type Conversion Edge Cases
Implicit Conversions
void test_type_conversion_edge_cases() {
// Integer promotion
char c = 127;
char d = 1;
// c + d is evaluated as int, not char
int sum = c + d;
assert(sum == 128); // Fine as int
// But assigning back can overflow
char result = c + d; // Overflow! Result is implementation-defined
// Signed/unsigned conversion
int signed_neg = -1;
unsigned int unsigned_val = signed_neg; // Large positive number!
assert(unsigned_val == UINT_MAX);
// Comparison of signed and unsigned
int s = -1;
unsigned int u = 1;
// if (s < u) { // Always false! -1 converted to large unsigned
// Floating point to integer truncation
double pi = 3.14159;
int truncated = (int)pi;
assert(truncated == 3);
double large = 1e20;
int overflow_int = (int)large; // Undefined behavior if doesn't fit!
}
Undefined Behavior
Common Undefined Behavior
void test_undefined_behavior() {
// 1. Null pointer dereference
int *null_ptr = NULL;
// *null_ptr = 42; // UB
// 2. Out of bounds array access
int arr[5];
// arr[10] = 42; // UB
// 3. Signed integer overflow
int max = INT_MAX;
// max + 1; // UB
// 4. Divide by zero
// int x = 10 / 0; // UB
// 5. Use after free
int *ptr = malloc(sizeof(int));
free(ptr);
// *ptr = 42; // UB
// 6. Double free
// free(ptr); // UB (already freed)
// 7. Uninitialized variable
int uninitialized;
// int value = uninitialized; // UB (indeterminate value)
// 8. Modifying string literal
char *literal = "hello";
// literal[0] = 'H'; // UB
// 9. Overlapping memory in strcpy/memcpy
char buffer[20] = "hello";
// strcpy(buffer + 1, buffer); // UB (overlapping)
// Use memmove for overlapping regions
memmove(buffer + 1, buffer, strlen(buffer) + 1);
// 10. Reading uninitialized memory
int *heap = malloc(sizeof(int));
// int val = *heap; // UB (not initialized)
free(heap);
}
C++ Specific Edge Cases
Object Lifetime
#include <memory>
#include <cassert>
void test_cpp_object_lifetime() {
// Dangling reference
int* ptr = nullptr;
{
int local = 42;
ptr = &local;
}
// *ptr is now dangling! (UB)
// Use after move
std::string str = "hello";
std::string moved = std::move(str);
// str is now in valid but unspecified state
// str.size(); // Safe but value is unspecified
// str.clear(); // Safe
}
Vector Edge Cases
#include <vector>
void test_vector_edge_cases() {
std::vector<int> vec;
// Empty vector
assert(vec.empty());
assert(vec.size() == 0);
// at() throws, operator[] doesn't
try {
vec.at(0); // Throws std::out_of_range
assert(false);
} catch (const std::out_of_range&) {
assert(true);
}
// operator[] doesn't check bounds (UB)
// int val = vec[0]; // Undefined behavior!
// Iterator invalidation
vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it == 3) {
vec.push_back(6); // Invalidates iterators! (UB)
// *it; // Undefined behavior
break;
}
}
// Safe modification
vec = {1, 2, 3, 4, 5};
for (size_t i = 0; i < vec.size(); ++i) {
if (vec[i] == 3) {
vec.push_back(6); // Safe with index
}
}
}
Smart Pointer Edge Cases
void test_smart_pointer_edge_cases() {
// Null unique_ptr
std::unique_ptr<int> null_ptr;
assert(null_ptr == nullptr);
assert(!null_ptr);
// Dereferencing null smart pointer (UB)
// *null_ptr; // Undefined behavior!
// Safe dereference
if (null_ptr) {
*null_ptr = 42;
}
// Double ownership (compile error)
std::unique_ptr<int> ptr1(new int(42));
// std::unique_ptr<int> ptr2 = ptr1; // Compile error (no copy)
std::unique_ptr<int> ptr2 = std::move(ptr1);
assert(ptr1 == nullptr); // Moved-from
// Shared_ptr cycles (memory leak)
struct Node {
std::shared_ptr<Node> next;
};
auto n1 = std::make_shared<Node>();
auto n2 = std::make_shared<Node>();
n1->next = n2;
n2->next = n1; // Circular reference! (leak)
// Use weak_ptr to break cycles
}
Testing Frameworks
Google Test Patterns
#include <gtest/gtest.h>
TEST(EdgeCaseTest, NullPointerHandling) {
int* null_ptr = nullptr;
EXPECT_EQ(null_ptr, nullptr);
EXPECT_FALSE(null_ptr);
// Test null handling
EXPECT_THROW(process_pointer(null_ptr), std::invalid_argument);
}
TEST(EdgeCaseTest, IntegerOverflow) {
EXPECT_EQ(INT_MAX, 2147483647);
EXPECT_EQ(INT_MIN, -2147483648);
// Check overflow detection
EXPECT_TRUE(will_overflow(INT_MAX, 1));
EXPECT_FALSE(will_overflow(100, 200));
}
TEST(EdgeCaseTest, ArrayBounds) {
int arr[5] = {1, 2, 3, 4, 5};
EXPECT_EQ(arr[0], 1);
EXPECT_EQ(arr[4], 5);
// Death test for out of bounds (ASAN/UBSAN will catch)
#ifdef __has_feature
#if __has_feature(address_sanitizer)
EXPECT_DEATH(arr[10], "");
#endif
#endif
}
// Parameterized tests for edge cases
class IntegerBoundaryTest : public testing::TestWithParam<int> {};
TEST_P(IntegerBoundaryTest, HandlesAllValues) {
int value = GetParam();
EXPECT_NO_THROW(process_integer(value));
}
INSTANTIATE_TEST_SUITE_P(
EdgeCases,
IntegerBoundaryTest,
testing::Values(
INT_MIN,
INT_MIN + 1,
-1,
0,
1,
INT_MAX - 1,
INT_MAX
)
);
// String edge cases
class StringEdgeCaseTest : public testing::TestWithParam<const char*> {};
TEST_P(StringEdgeCaseTest, HandlesEdgeCases) {
const char* str = GetParam();
if (str == nullptr) {
EXPECT_THROW(process_string(str), std::invalid_argument);
} else {
EXPECT_NO_THROW(process_string(str));
}
}
INSTANTIATE_TEST_SUITE_P(
StringEdges,
StringEdgeCaseTest,
testing::Values(
nullptr,
"",
"a",
"very long string with many characters to test buffer handling",
"\n\t\r",
"unicode: 你好"
)
);
Testing with Sanitizers
// Compile with sanitizers to catch edge cases:
// -fsanitize=address (AddressSanitizer - memory errors)
// -fsanitize=undefined (UndefinedBehaviorSanitizer - UB)
// -fsanitize=thread (ThreadSanitizer - race conditions)
TEST(SanitizerTest, DetectsBufferOverflow) {
char buffer[10];
// ASAN will catch this
// buffer[10] = 'x'; // Buffer overflow
// Safe access
buffer[9] = 'x';
EXPECT_EQ(buffer[9], 'x');
}
TEST(SanitizerTest, DetectsUseAfterFree) {
int* ptr = new int(42);
delete ptr;
// ASAN will catch this
// *ptr = 10; // Use after free
// Safe: don't use after delete
}
TEST(SanitizerTest, DetectsUndefinedBehavior) {
int max = INT_MAX;
// UBSAN will catch this
// int overflow = max + 1; // Signed overflow (UB)
// Safe: check before operation
bool would_overflow = (max > INT_MAX - 1);
EXPECT_TRUE(would_overflow);
}
Valgrind Integration
# Run tests with Valgrind to detect memory issues
valgrind --leak-check=full --track-origins=yes ./my_tests
# Example Valgrind checks:
# - Memory leaks
# - Use of uninitialized values
# - Invalid memory access
# - Double free
# - Mismatched allocation/deallocation
Helper Functions
// Helper for safe string operations
inline size_t safe_strlen(const char* str) {
return str ? strlen(str) : 0;
}
// Helper for overflow detection
inline bool add_would_overflow(int a, int b) {
if (a > 0 && b > 0) {
return a > INT_MAX - b;
}
if (a < 0 && b < 0) {
return a < INT_MIN - b;
}
return false;
}
// Helper for safe array access
template<typename T, size_t N>
inline bool is_valid_index(const T (&arr)[N], size_t idx) {
return idx < N;
}