Why does a string function parameter need a memory keyword when a uint256 parameter does not?
Because a string is secretly an array of bytes, and Solidity requires a data location (memory/calldata) only for reference types — arrays, structs, and mappings — while it handles simple value types like uint256 automatically.
function addPerson(string memory _name, uint256 _num) public { ... }
// ^^^^^^ required (no location needed)
Solidity knows on its own where a uint256 should live during a function, so adding memory to it is actually an error. But a string is an array of bytes under the hood, and arrays/structs/mappings are complex enough that the compiler makes you state explicitly whether they sit in memory or calldata. Forgetting it gives: "data location must be memory or calldata for parameter." The rule: structs, mappings, and arrays (and therefore strings) need the keyword; plain value types do not.
Go deeper:
Solidity docs: bytes and string as arrays — why strings are reference types that need a data location.